├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appsync-logo-600.png ├── examples ├── cdk │ ├── aoss-search-app │ │ ├── .eslintignore │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin │ │ │ └── aoss-search-app.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib │ │ │ ├── aoss-search-app-stack.ts │ │ │ └── appsync │ │ │ │ ├── .graphqlconfig.yml │ │ │ │ ├── codegen │ │ │ │ └── index.ts │ │ │ │ ├── resolvers │ │ │ │ ├── Mutation.createIndex.[aoss].ts │ │ │ │ ├── Mutation.indexTodo.[aoss].ts │ │ │ │ ├── Query.search.[aoss].ts │ │ │ │ └── utils.ts │ │ │ │ └── schema.graphql │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ │ └── aos-ddb-search-app.test.ts │ │ └── tsconfig.json │ ├── constructs │ │ └── appsync-helper │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── README.md │ │ │ ├── jest.config.js │ │ │ ├── lib │ │ │ └── index.ts │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── test │ │ │ └── appsync-helper.test.ts │ │ │ └── tsconfig.json │ ├── dynamodb-todo-app │ │ ├── .eslintignore │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin │ │ │ └── dynamodb-todo-app.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib │ │ │ ├── appsync │ │ │ │ ├── .graphqlconfig.yml │ │ │ │ ├── codegen │ │ │ │ │ └── index.ts │ │ │ │ ├── resolvers │ │ │ │ │ ├── Mutation.createTodo.[todos].ts │ │ │ │ │ ├── Mutation.deleteTodo.[todos].ts │ │ │ │ │ ├── Mutation.updateTodo.[todos].ts │ │ │ │ │ ├── Query.getTodo.[todos].ts │ │ │ │ │ ├── Query.listTodos.[todos].ts │ │ │ │ │ └── utils.ts │ │ │ │ └── schema.graphql │ │ │ └── dynamodb-todo-app-stack.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ │ └── dynamodb-todo-app.test.ts │ │ └── tsconfig.json │ └── pub-sub-app │ │ ├── .eslintignore │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin │ │ └── pub-sub-app.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib │ │ ├── appsync │ │ │ ├── .graphqlconfig.yml │ │ │ ├── codegen │ │ │ │ └── index.ts │ │ │ ├── resolvers │ │ │ │ ├── Mutation.publish.[NONE].ts │ │ │ │ ├── Query.whoami.[NONE].ts │ │ │ │ └── Subscription.onPublish.[NONE].ts │ │ │ └── schema.graphql │ │ └── pub-sub-app-stack.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ └── pub-sub-app.test.ts │ │ └── tsconfig.json ├── cloudformation │ ├── todo-api-cfn │ │ ├── README.md │ │ ├── resolvers │ │ │ ├── createTodo.js │ │ │ ├── deleteTodo.js │ │ │ ├── getTodo.js │ │ │ ├── listTodos.js │ │ │ ├── queryTodosByOwner.js │ │ │ └── updateTodo.js │ │ ├── schema.graphql │ │ └── template.yaml │ └── todo-api-pipeline-cfn │ │ ├── README.md │ │ ├── functions │ │ ├── createItem.js │ │ ├── deleteItem.js │ │ ├── getItem.js │ │ ├── listItems.js │ │ ├── out.js │ │ ├── queryItems.js │ │ ├── scratch.js │ │ └── udpateItem.js │ │ ├── resolvers │ │ └── default.js │ │ ├── schema.graphql │ │ └── template.yaml ├── serverless │ └── lambda-http-ddb-datasources │ │ ├── README.md │ │ ├── lambda │ │ └── index.js │ │ ├── package.json │ │ ├── resolvers │ │ ├── ddb │ │ │ ├── addTodo.js │ │ │ ├── getTodo.js │ │ │ └── listTodos.js │ │ ├── http │ │ │ ├── getUser.js │ │ │ └── listUsers.js │ │ └── lambda │ │ │ ├── getPost.js │ │ │ └── listPosts.js │ │ ├── schema.graphql │ │ └── serverless.yml └── terraform │ ├── .terraform.lock.hcl │ ├── main.tf │ ├── provider.tf │ ├── resolvers │ ├── getTodo.js │ └── listTodos.js │ └── schema.graphql ├── package-lock.json ├── package.json ├── samples ├── NONE │ ├── enhancedSubscription.js │ └── localPublish.js ├── dynamodb │ ├── batch │ │ ├── batchDeleteItems.js │ │ ├── batchGetItems.js │ │ └── batchPutItems.js │ ├── general │ │ ├── deleteItem.js │ │ ├── getItem.js │ │ ├── listItems.js │ │ ├── putItem.js │ │ ├── updateIncrementCount.js │ │ └── updateItem.js │ └── queries │ │ ├── all-items-today.js │ │ ├── pagination.js │ │ ├── simple-query.js │ │ ├── with-contains-expression.js │ │ ├── with-filter-on-index.js │ │ └── with-greater-than.js ├── eventbridge │ └── simple.js ├── http │ ├── forward.js │ ├── getToApiGW.js │ ├── publishToSNS.js │ ├── putToApiGW.js │ └── translate.js ├── lambda │ └── invoke.js ├── opensearch │ ├── geo.js │ ├── getDocumentByID.js │ ├── paginate.js │ └── simpleTermQuery.js ├── package-lock.json ├── package.json ├── pipeline │ └── default.js └── rds │ ├── README.md │ └── queries │ ├── invoice.js │ ├── invoices.js │ ├── playlist_count.js │ ├── sales.js │ ├── select.js │ ├── sql.js │ └── subquery.js └── scripts ├── evaluate.sh └── evaluate ├── index.mjs ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | .DS_Store 146 | scripts/response.json 147 | 148 | # CDK asset staging directory 149 | .cdk.staging 150 | cdk.out 151 | 152 | ## Terraform 153 | 154 | # Local .terraform directories 155 | **/.terraform/* 156 | 157 | # .tfstate files 158 | *.tfstate 159 | *.tfstate.* 160 | 161 | # Crash log files 162 | crash.log 163 | crash.*.log 164 | 165 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 166 | # password, private keys, and other secrets. These should not be part of version 167 | # control as they are data points which are potentially sensitive and subject 168 | # to change depending on the environment. 169 | *.tfvars 170 | *.tfvars.json 171 | 172 | # Ignore override files as they are usually used to override resources locally and so 173 | # are not checked in 174 | override.tf 175 | override.tf.json 176 | *_override.tf 177 | *_override.tf.json 178 | 179 | # Ignore transient lock info files created by terraform apply 180 | .terraform.tfstate.lock.info 181 | 182 | # Include override files you do wish to add to version control using negated pattern 183 | # !example_override.tf 184 | 185 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 186 | # example: *tfplan* 187 | 188 | # Ignore CLI configuration files 189 | .terraformrc 190 | terraform.rc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "useTabs": true, 6 | "singleQuote": true, 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ], 7 | "cSpell.words": ["aoss", "codegen", "todos"] 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | * A reproducible test case or series of steps 17 | * The version of our code being used 18 | * Any modifications you've made relevant to the bug 19 | * Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the *main* branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

AWS AppSync GraphQL Samples

5 | 6 |

7 | 8 | Welcome to the AWS AppSync JavaScript Samples repository! This collection of samples provides clear and concise examples of utilizing AppSync JavaScript resolvers and functions with various data sources on AWS AppSync. These samples are here to help you get started quickly and enable you to kickstart your own projects. 9 | 10 | You can use this repository to get started in both TypeScript and JavaScript solutions. 11 | 12 | **[Documentation](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html)** | **[npm - appsync-utils](https://www.npmjs.com/package/@aws-appsync/utils)** | **[npm - eslint plugin](https://www.npmjs.com/package/@aws-appsync/eslint-plugin)** | **[Samples](./samples/)** | **[CloudFormation and CDK Examples](./examples/)** 13 | 14 | ## Table of contents 15 | 16 | - [About resolvers](#about-resolvers) 17 | - [Content](#content) 18 | - [Features](#features) 19 | - [Getting started](#getting-started) 20 | - [Working locally](#working-locally) 21 | - [In the console](#in-the-console) 22 | - [Feedback and Support](#feedback-and-support) 23 | 24 | ## About resolvers 25 | 26 | Resolvers are the connectors between GraphQL and a data source. They tell AWS AppSync how to translate an incoming GraphQL request into instructions for your backend data source, and how to translate the response from that data source back into a GraphQL response. With AWS AppSync, you can write resolvers using JavaScript, that are run on the AppSync JavaScript (APPSYNC_JS) runtime. 27 | 28 | The AppSync JavaScript runtime allows developers to write expressive logic in JavaScript for their business requirements, while using syntax, constructs, and features of the language that they are already familiar with. The runtime has some restrictions from tradeoffs that were made in order for to provide high performance and consistent execution in a secure, multi-tenant environment in a serverless manner. However, the tradeoffs do come with benefits as well, such as no extra costs and lower latencies. For a detailed list of supported features in the runtime, see our [resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference-js.html). 29 | 30 | JavaScript resolvers allow you to express your business logic using a simple interface. At its core, your resolvers implement a `request` and `response` function. 31 | 32 | ```js 33 | export function request(ctx) { 34 | return {}; 35 | } 36 | 37 | export function response(ctx) { 38 | return ctx.result; 39 | } 40 | ``` 41 | 42 | ## Content 43 | 44 | - [examples](./samples/): Contains AWS Cloudformation, AWS CDK and other IaC platforms sample APIs 45 | - [samples](./samples/) Contains AppSync resolver and functions templates that you can use in your own APIs 46 | 47 | ## Features 48 | 49 | For a full list of supported feature, see our [runtime features documentation](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference-js.html). 50 | 51 | ## Getting started 52 | 53 | If you are new to AppSync, get an overview in our [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html). 54 | 55 | ### Working locally 56 | 57 | You can get started with one of the CDK [examples](./examples/cdk/). Simply clone this repo and start with one of the example directories. Run `npm install` to install all the dependencies. You can review the code and run `npm run cdk deploy` to launch the application. 58 | 59 | The best way to work locally in your own project is to start by installing the two libraries: 60 | 61 | - [@aws-appsync/utils](https://www.npmjs.com/package/@aws-appsync/utils) - Provides type validation and autocompletion in code editors. 62 | - [@aws-appsync/eslint-plugin](https://www.npmjs.com/package/@aws-appsync/eslint-plugin) - Catches and fixes problems quickly during development. 63 | 64 | Find out more [here](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#utility-resolvers). 65 | 66 | ### In the console 67 | 68 | You can also work from the console and edit your code directly the console editor. Note that TypeScript is not supported from the console. 69 | 70 | ## Feedback and Support 71 | 72 | If you have any questions, feedback, or need assistance, please don't hesitate to open an issue on this repository. For general inquiries about AppSync, please visit our [Community repository](https://github.com/aws/aws-appsync-community) 73 | 74 | --- 75 | 76 | This library is licensed under the MIT-0 License. See the LICENSE file. 77 | -------------------------------------------------------------------------------- /appsync-logo-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-appsync-resolver-samples/e3d22611333bdb2cf5f580dce1948ab38c61dab2/appsync-logo-600.png -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": {}, 14 | "overrides": [ 15 | { 16 | "files": ["lib/appsync/resolvers/*.ts"], 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:@aws-appsync/recommended" 21 | ], 22 | "parserOptions": { 23 | "ecmaVersion": "latest", 24 | "sourceType": "module", 25 | "project": "./tsconfig.json" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | **/codegen/graphql 11 | **/codegen/API.ts 12 | output.json -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/README.md: -------------------------------------------------------------------------------- 1 | # Todo Search API - CDK Example 2 | 3 | This CDK app provides an implementation of an AppSync API that uses an [Amazon OpenSearch Serverless](https://aws.amazon.com/opensearch-service/features/serverless/) Collection as a data source. The data source is configured as an HTTP data source and uses an attached role to sign the requests. 4 | 5 | This advanced example demonstrates how you can connect to just about any data source using an HTTP resolver, including any AWS service. 6 | 7 | The API allows you to create an index, create a document, and search documents. The schema uses a Todo type as an example. 8 | 9 | ```graphql 10 | type Todo { 11 | id: ID! 12 | title: String 13 | description: String 14 | owner: String 15 | } 16 | ``` 17 | 18 | The app uses the [AppSync Helper construct](../constructs/appsync-helper/) for a quick setup. 19 | 20 | ```sh 21 | lib 22 | ├── appsync 23 | │ ├── codegen 24 | │ │ ├── graphql 25 | │ │ │ ├── mutations.ts 26 | │ │ │ └── queries.ts 27 | │ │ ├── API.ts 28 | │ │ └── index.ts 29 | │ ├── resolvers 30 | │ │ ├── Mutation.createIndex.[aoss].ts 31 | │ │ ├── Mutation.indexTodo.[aoss].ts 32 | │ │ ├── Query.search.[aoss].ts 33 | │ │ └── utils.ts 34 | │ └── schema.graphql 35 | ├── appsync-helper.ts 36 | └── aoss-search-app-stack.ts 37 | ``` 38 | 39 | The [appsync](./lib/appsync-helper.ts) folder contains schema and the resolver code. This project uses unit resolvers, but you can expand it to use pipeline resolvers if needed. 40 | 41 | ## Pre-req 42 | 43 | Build the `appsync-helper`. See the [README](../constructs/appsync-helper/README.md#init). 44 | 45 | ## Init 46 | 47 | Install all the dependencies. From the [project folder](./) 48 | 49 | ```sh 50 | npm install 51 | ``` 52 | 53 | ## Codegen 54 | 55 | This TypeScript projects uses types generated by [Amplify Codegen](https://docs.amplify.aws/cli-legacy/graphql-transformer/codegen/). To populate your codegen and generate it after you update your schema, run the command: 56 | 57 | ```sh 58 | npm run codegen 59 | ``` 60 | 61 | ## Deploy the stack 62 | 63 | To deploy the stack, from the top folder: 64 | 65 | ```sh 66 | npm run cdk deploy -- -O output.json 67 | ``` 68 | 69 | Once deployed, you can find your API **SearchTodoAPI** in the AWS console. 70 | 71 | ## Test the API 72 | 73 | First, begin by creating an index: 74 | 75 | ```graphql 76 | mutation CreateIndex { 77 | createIndex(index: "todos") 78 | } 79 | ``` 80 | 81 | Next, create a document in the index. 82 | 83 | ```graphql 84 | mutation IndexDoc { 85 | indexTodo(input: {description: "a new todo", id: "a-given-id", owner: "John", title: "things need doing"}) 86 | } 87 | ``` 88 | 89 | Then, lets search for some items. Lets find all the todos that belong to owners who's name begin with 'john': 90 | 91 | ```graphql 92 | query Search { 93 | search(filter: {owner: {regexp: "john.*"}}) { 94 | items { 95 | id 96 | owner 97 | title 98 | description 99 | } 100 | nextToken 101 | total 102 | } 103 | } 104 | ``` 105 | 106 | ## Connect to an app 107 | 108 | You can use the info in [./output.json](./output.json) to configure your Amplify client in your app. See [Building a client application](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app.html). 109 | 110 | ## Destroy the stack 111 | 112 | To destroy the stack and all the created resources: 113 | 114 | ```sh 115 | npm run cdk destroy 116 | ``` 117 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/bin/aoss-search-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AossSearchAppStack } from '../lib/aoss-search-app-stack'; 5 | 6 | const app = new cdk.App(); 7 | new AossSearchAppStack(app, 'AossSearchAppStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | /* Uncomment the next line to specialize this stack for the AWS Account 12 | * and Region that are implied by the current CLI configuration. */ 13 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 14 | /* Uncomment the next line if you know exactly what Account and Region you 15 | * want to deploy the stack to. */ 16 | // env: { account: '123456789012', region: 'us-east-1' }, 17 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 18 | }); 19 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/aoss-search-app.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true, 44 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 45 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 46 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 47 | "@aws-cdk/aws-kms:aliasNameRef": true, 48 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/aoss-search-app-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { AppSyncHelper } from 'appsync-helper'; 4 | import path = require('node:path'); 5 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import * as opensearchserverless from 'aws-cdk-lib/aws-opensearchserverless'; 8 | 9 | export class AossSearchAppStack extends cdk.Stack { 10 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | const httpDatasourceRole = new iam.Role(this, 'Role', { 14 | assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), 15 | description: 'role for http resource', 16 | }); 17 | httpDatasourceRole.addToPolicy( 18 | new iam.PolicyStatement({ actions: ['aoss:*'], resources: ['*'] }) 19 | ); 20 | 21 | const appSyncApp = new AppSyncHelper(this, 'SearchTodoAPI', { 22 | basedir: path.join(__dirname, 'appsync'), 23 | logConfig: { 24 | fieldLogLevel: appsync.FieldLogLevel.ALL, 25 | excludeVerboseContent: false, 26 | retention: cdk.aws_logs.RetentionDays.ONE_WEEK, 27 | }, 28 | xrayEnabled: true, 29 | }); 30 | 31 | const collectionName = 'todos'; 32 | 33 | const encPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'EncPolicy', { 34 | name: 'encryption-policy', 35 | type: 'encryption', 36 | policy: JSON.stringify({ 37 | Rules: [{ ResourceType: 'collection', Resource: [`collection/${collectionName}`] }], 38 | AWSOwnedKey: true, 39 | }), 40 | }); 41 | 42 | const netPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'NetworkPolicy', { 43 | name: 'network-default', 44 | type: 'network', 45 | policy: JSON.stringify([ 46 | { 47 | Rules: [ 48 | { ResourceType: 'collection', Resource: [`collection/${collectionName}`] }, 49 | { ResourceType: 'dashboard', Resource: [`collection/${collectionName}`] }, 50 | ], 51 | AllowFromPublic: true, 52 | }, 53 | ]), 54 | }); 55 | 56 | const dataAccessPolicy = new opensearchserverless.CfnAccessPolicy(this, 'AccessPolicy', { 57 | name: 'default', 58 | type: 'data', 59 | policy: JSON.stringify([ 60 | { 61 | Description: 'Access for appsync', 62 | Rules: [ 63 | { ResourceType: 'index', Resource: ['index/*/*'], Permission: ['aoss:*'] }, 64 | { 65 | ResourceType: 'collection', 66 | Resource: [`collection/${collectionName}`], 67 | Permission: ['aoss:*'], 68 | }, 69 | ], 70 | Principal: [httpDatasourceRole.roleArn], 71 | }, 72 | ]), 73 | }); 74 | 75 | const collection = new opensearchserverless.CfnCollection(this, 'todos-collection', { 76 | name: collectionName, 77 | description: 'a collection of todos', 78 | type: 'SEARCH', 79 | }); 80 | collection.addDependency(encPolicy); 81 | collection.addDependency(netPolicy); 82 | collection.addDependency(dataAccessPolicy); 83 | 84 | const aossDS = new appsync.CfnDataSource(appSyncApp, 'aoss', { 85 | apiId: appSyncApp.apiId, 86 | name: 'aoss', 87 | serviceRoleArn: httpDatasourceRole.roleArn, 88 | type: 'HTTP', 89 | httpConfig: { 90 | endpoint: collection.attrCollectionEndpoint, 91 | authorizationConfig: { 92 | authorizationType: 'AWS_IAM', 93 | awsIamConfig: { 94 | signingRegion: cdk.Stack.of(this).region, 95 | signingServiceName: 'aoss', 96 | }, 97 | }, 98 | }, 99 | }); 100 | appSyncApp.addCfnDataSource(aossDS); 101 | appSyncApp.bind(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | Codegen Project: 3 | schemaPath: schema.graphql 4 | includes: 5 | - codegen/graphql/**/*.ts 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: typescript 11 | generatedFileName: codegen/API.ts 12 | docsFilePath: codegen/graphql 13 | region: us-east-1 14 | apiId: null 15 | frontend: javascript 16 | framework: none 17 | maxDepth: 2 18 | extensions: 19 | amplify: 20 | version: 3 21 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/codegen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './API'; 2 | export type Result = Omit; 3 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/resolvers/Mutation.createIndex.[aoss].ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@aws-appsync/utils'; 2 | import { CreateIndexMutationVariables } from '../codegen'; 3 | import { createIndex } from './utils'; 4 | 5 | export function request(ctx: Context) { 6 | const index = ctx.args.index; 7 | // optional settings 8 | const body = { 9 | settings: { 10 | index: { number_of_shards: 4, number_of_replicas: 3 }, 11 | }, 12 | }; 13 | return createIndex({ index, body }); 14 | } 15 | 16 | export function response(ctx: Context) { 17 | const body = JSON.parse(ctx.result.body); 18 | if (ctx.result.statusCode !== 200) { 19 | util.error(body.error?.reason, body.error?.type, body); 20 | } 21 | return body; 22 | } 23 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/resolvers/Mutation.indexTodo.[aoss].ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@aws-appsync/utils'; 2 | import { IndexTodoMutationVariables } from '../codegen'; 3 | import { indexItem } from './utils'; 4 | 5 | export function request(ctx: Context) { 6 | return indexItem({ 7 | id: ctx.args.input.id, 8 | index: 'todos', 9 | body: ctx.args.input, 10 | }); 11 | } 12 | 13 | export function response(ctx: Context) { 14 | const body = JSON.parse(ctx.result.body); 15 | if (!`${ctx.result.statusCode}`.startsWith('2')) { 16 | util.error(body.error?.reason, body.error?.type, body); 17 | } 18 | return body; 19 | } 20 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/resolvers/Query.search.[aoss].ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@aws-appsync/utils'; 2 | import { search } from './utils'; 3 | import { Result, SearchQueryVariables, SearchableTodoConnection } from '../codegen'; 4 | 5 | export function request(ctx: Context) { 6 | const { filter, limit, nextToken } = ctx.args; 7 | const match_all = JSON.stringify({ match_all: {} }); 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const bq = !filter ? match_all : util.transform.toElasticsearchQueryDSL(filter as any); 10 | const body: Record = { query: JSON.parse(bq) }; 11 | if (nextToken) { 12 | body.search_after = JSON.parse(util.base64Decode(nextToken)); 13 | } 14 | return search({ 15 | index: 'todos', 16 | body, 17 | size: limit ?? 100, 18 | sort: 'id.keyword:asc', 19 | }); 20 | } 21 | 22 | export function response(ctx: Context): Result { 23 | const body = JSON.parse(ctx.result.body); 24 | if (!`${ctx.result.statusCode}`.startsWith('2')) { 25 | util.error(body.error?.reason, body.error?.type, body); 26 | } 27 | let nextToken = null; 28 | const hits = body.hits.hits; 29 | if (hits.length > 0) { 30 | nextToken = util.base64Encode(JSON.stringify(hits[hits.length - 1].sort)); 31 | } 32 | return { 33 | items: hits.map((hit: { _source: unknown }) => hit._source), 34 | total: body.hits.total.value, 35 | nextToken, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/resolvers/utils.ts: -------------------------------------------------------------------------------- 1 | import { HTTPRequest } from '@aws-appsync/utils'; 2 | 3 | export function createIndex(params: IndicesCreate) { 4 | const headers = { 'content-type': 'application/json' }; 5 | const { body, index, ...query } = params; 6 | const path = '/' + util.urlEncode(index); 7 | return fetch(path, { method: 'PUT', body, query, headers }); 8 | } 9 | 10 | export function indexItem(params: Index) { 11 | const headers = { 'content-type': 'application/json' }; 12 | const { body, index, id, ...query } = params; 13 | if (id === null) { 14 | util.error('Missing required parameter: id'); 15 | } 16 | if (index == null) { 17 | util.error('Missing required parameter: index'); 18 | } 19 | if (body == null) { 20 | util.error('Missing required parameter: body'); 21 | } 22 | 23 | const path = '/' + util.urlEncode(index) + '/' + '_create' + '/' + util.urlEncode(id!); 24 | return fetch(path, { method: 'PUT', body, query, headers }); 25 | } 26 | 27 | export function search(params: Search) { 28 | const headers = { 'content-type': 'application/json' }; 29 | if (params.body == null) { 30 | util.error('Missing required parameter: body'); 31 | } 32 | 33 | const { body, index, ...queryString } = params; 34 | let path = '/_search'; 35 | if (index !== null) { 36 | path = '/' + util.urlEncode(index) + '/' + '_search'; 37 | } else { 38 | path = '/' + '_search'; 39 | } 40 | 41 | const query: Record = {}; 42 | Object.entries(queryString).forEach(([k, v]) => { 43 | query[k] = util.urlEncode(`${v}`); 44 | }); 45 | 46 | return fetch(path, { method: 'POST', body, query, headers }); 47 | } 48 | 49 | type RequestBody> = T; 50 | 51 | export interface Index { 52 | id?: string; 53 | index: string; 54 | wait_for_active_shards?: string; 55 | op_type?: 'index' | 'create'; 56 | refresh?: 'wait_for' | boolean; 57 | routing?: string; 58 | timeout?: string; 59 | version?: number; 60 | version_type?: 'internal' | 'external' | 'external_gte'; 61 | if_seq_no?: number; 62 | if_primary_term?: number; 63 | pipeline?: string; 64 | require_alias?: boolean; 65 | body: T; 66 | } 67 | 68 | export interface IndicesCreate { 69 | index: string; 70 | wait_for_active_shards?: string; 71 | timeout?: string; 72 | cluster_manager_timeout?: string; 73 | body?: T; 74 | } 75 | export interface Search { 76 | index: string; 77 | _source_exclude?: string | string[]; 78 | _source_include?: string | string[]; 79 | analyzer?: string; 80 | analyze_wildcard?: boolean; 81 | ccs_minimize_roundtrips?: boolean; 82 | default_operator?: 'AND' | 'OR'; 83 | df?: string; 84 | explain?: boolean; 85 | stored_fields?: string | string[]; 86 | docvalue_fields?: string | string[]; 87 | from?: number; 88 | ignore_unavailable?: boolean; 89 | ignore_throttled?: boolean; 90 | allow_no_indices?: boolean; 91 | expand_wildcards?: 'open' | 'closed' | 'hidden' | 'none' | 'all'; 92 | lenient?: boolean; 93 | preference?: string; 94 | q?: string; 95 | routing?: string | string[]; 96 | scroll?: string; 97 | search_type?: 'query_then_fetch' | 'dfs_query_then_fetch'; 98 | size?: number; 99 | sort?: string | string[]; 100 | _source?: string | string[]; 101 | _source_excludes?: string | string[]; 102 | _source_includes?: string | string[]; 103 | terminate_after?: number; 104 | stats?: string | string[]; 105 | suggest_field?: string; 106 | suggest_mode?: 'missing' | 'popular' | 'always'; 107 | suggest_size?: number; 108 | suggest_text?: string; 109 | timeout?: string; 110 | track_scores?: boolean; 111 | track_total_hits?: boolean; 112 | allow_partial_search_results?: boolean; 113 | typed_keys?: boolean; 114 | version?: boolean; 115 | seq_no_primary_term?: boolean; 116 | request_cache?: boolean; 117 | batched_reduce_size?: number; 118 | max_concurrent_shard_requests?: number; 119 | pre_filter_shard_size?: number; 120 | rest_total_hits_as_int?: boolean; 121 | min_compatible_shard_node?: string; 122 | body?: T; 123 | } 124 | 125 | export type FetchOptions = { 126 | method: 'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'; 127 | headers?: Record; 128 | body: unknown; 129 | query?: Record; 130 | }; 131 | 132 | /** 133 | * Sends an HTTP request 134 | */ 135 | export function fetch(resourcePath: string, options: FetchOptions): HTTPRequest { 136 | const { method = 'GET', headers, body, query } = options; 137 | const request: HTTPRequest = { 138 | resourcePath, 139 | method, 140 | params: { headers }, 141 | }; 142 | 143 | if (body) { 144 | request.params!.body = JSON.stringify(body); 145 | } 146 | if (query) { 147 | request.params!.query = query; 148 | } 149 | 150 | return request; 151 | } 152 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/lib/appsync/schema.graphql: -------------------------------------------------------------------------------- 1 | type Todo { 2 | id: ID! 3 | title: String 4 | description: String 5 | owner: String 6 | } 7 | 8 | input IndexTodoInput { 9 | id: ID! 10 | title: String 11 | description: String 12 | owner: String 13 | } 14 | 15 | type Mutation { 16 | createIndex(index: String!): AWSJSON 17 | indexTodo(input: IndexTodoInput!): AWSJSON 18 | } 19 | 20 | type Query { 21 | getTodo(id: ID!): Todo 22 | search(filter: SearchableTodoFilterInput, nextToken: String, limit: Int): SearchableTodoConnection 23 | } 24 | 25 | type SearchableTodoConnection { 26 | items: [Todo]! 27 | nextToken: String 28 | total: Int 29 | } 30 | 31 | input SearchableStringFilterInput { 32 | ne: String 33 | gt: String 34 | lt: String 35 | gte: String 36 | lte: String 37 | eq: String 38 | match: String 39 | matchPhrase: String 40 | matchPhrasePrefix: String 41 | multiMatch: String 42 | exists: Boolean 43 | wildcard: String 44 | regexp: String 45 | range: [String] 46 | } 47 | 48 | input SearchableIntFilterInput { 49 | ne: Int 50 | gt: Int 51 | lt: Int 52 | gte: Int 53 | lte: Int 54 | eq: Int 55 | range: [Int] 56 | } 57 | 58 | input SearchableFloatFilterInput { 59 | ne: Float 60 | gt: Float 61 | lt: Float 62 | gte: Float 63 | lte: Float 64 | eq: Float 65 | range: [Float] 66 | } 67 | 68 | input SearchableBooleanFilterInput { 69 | eq: Boolean 70 | ne: Boolean 71 | } 72 | 73 | input SearchableIDFilterInput { 74 | ne: ID 75 | gt: ID 76 | lt: ID 77 | gte: ID 78 | lte: ID 79 | eq: ID 80 | match: ID 81 | matchPhrase: ID 82 | matchPhrasePrefix: ID 83 | multiMatch: ID 84 | exists: Boolean 85 | wildcard: ID 86 | regexp: ID 87 | range: [ID] 88 | } 89 | 90 | input SearchableTodoFilterInput { 91 | id: SearchableIDFilterInput 92 | title: SearchableStringFilterInput 93 | description: SearchableStringFilterInput 94 | owner: SearchableStringFilterInput 95 | and: [SearchableTodoFilterInput] 96 | or: [SearchableTodoFilterInput] 97 | not: SearchableTodoFilterInput 98 | } 99 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aoss-search-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "aoss-search-app": "bin/aoss-search-app.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "prebuild": "$npm_execpath run codegen", 13 | "codegen": "cd lib/appsync && npx --package=@aws-amplify/cli amplify codegen" 14 | }, 15 | "devDependencies": { 16 | "@aws-appsync/eslint-plugin": "^1.2.6", 17 | "@aws-appsync/utils": "1.2.6", 18 | "@graphql-tools/schema": "^10.0.0", 19 | "@types/jest": "^29.5.1", 20 | "@types/node": "20.1.7", 21 | "@typescript-eslint/eslint-plugin": "^6.3.0", 22 | "@typescript-eslint/parser": "^6.3.0", 23 | "appsync-helper": "file:../constructs/appsync-helper", 24 | "aws-cdk": "2.87.0", 25 | "esbuild": "^0.18.17", 26 | "eslint": "^8.46.0", 27 | "graphql": "^16.7.1", 28 | "jest": "^29.5.0", 29 | "ts-jest": "^29.1.0", 30 | "ts-node": "^10.9.1", 31 | "typescript": "~5.1.3" 32 | }, 33 | "dependencies": { 34 | "aws-cdk-lib": "2.87.0", 35 | "constructs": "^10.0.0", 36 | "source-map-support": "^0.5.21" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/test/aos-ddb-search-app.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as AosDdbSearchApp from '../lib/aoss-search-app-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/aoss-search-app-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new AosDdbSearchApp.AosDdbSearchAppStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | // template.hasResourceProperties('AWS::SQS::Queue', { 14 | // VisibilityTimeout: 300 15 | // }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/cdk/aoss-search-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020", "dom"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/README.md: -------------------------------------------------------------------------------- 1 | # AppSync Helper Library 2 | 3 | *a helper library to help you get started with your AppSync API and TypeScript resolvers.* 4 | 5 | This library helps you: 6 | 7 | - Get started with AppSync resources in your CDK project 8 | - Automatically builds your TypeScript resolvers 9 | - Links your resolvers to your data sources 10 | 11 | ## Init 12 | 13 | First install dependencies and build the project: 14 | 15 | ```sh 16 | npm run init 17 | ``` 18 | 19 | ## Getting started 20 | 21 | You can find an example using this construct [here](../../dynamodb-todo-app/). 22 | 23 | To use the helper, first install it: 24 | 25 | ```sh 26 | npm link appsync-helper -D 27 | ``` 28 | 29 | Simply pass the path to your main appsync folder in the `AppSyncHelper` props. 30 | 31 | ```typescript 32 | const appSyncApp = new AppSyncHelper(this, 'TodoAPI', { 33 | basedir: path.join(__dirname, 'appsync'), 34 | logConfig: { fieldLogLevel: FieldLogLevel.ALL }, 35 | xrayEnabled: true, 36 | }); 37 | 38 | // add data sources 39 | appSyncApp.addNoneDataSource('NONE'); 40 | appSyncApp.addDynamoDbDataSource('todos', todoTable); 41 | 42 | // Important!!! call bind to bring it all together 43 | appSyncApp.bind(); 44 | ``` 45 | 46 | ### Expected folder structure 47 | 48 | The helper finds your schema, resolvers, and functions based on an expected directory structure 49 | 50 | ```sh 51 | lib 52 | ├── appsync 53 | │ ├── resolvers 54 | │ │ ├── Mutation.createTodo.[todos].ts 55 | │ │ ├── Mutation.deleteTodo.[todos].ts 56 | │ │ ├── Mutation.updateTodo.[todos].ts 57 | │ │ ├── Query.getTodo.[todos].ts 58 | │ │ ├── Query.listTodos.[todos].ts 59 | │ │ ├── Query.queryByOwner 60 | │ │ │ ├── 1.[todos].ts 61 | │ │ │ └── 2.[NONE].ts 62 | │ │ └── utils.ts 63 | │ └── schema.graphql 64 | └── dynamodb-todo-app-stack.ts 65 | ``` 66 | 67 | Your unit resolver files follow this naming format: 68 | 69 | ```text 70 | ..[].ts 71 | ``` 72 | 73 | Your pipeline resolvers are a folder with this format: 74 | 75 | ```text 76 | . 77 | ``` 78 | 79 | and an optional `index.ts` in the folder as the pipeline resolver code. If no `index.ts` is present the following default code is used: 80 | 81 | ```ts 82 | export function request(){ return {} } 83 | export function response(ctx){ return ctx.prev.result} 84 | ``` 85 | 86 | The functions in your pipeline folder have this format: 87 | 88 | ```text 89 | .[].ts 90 | ``` 91 | 92 | That's it. The construct validates your schema and ties your resolvers to the named data sources. If your schema is invalid or if your resolvers are linked to data sources that do not exist, the cdk synth step will fail. This saves your from an unnecessary deployment. 93 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { 3 | AppsyncFunction, 4 | Code, 5 | FunctionRuntime, 6 | GraphqlApi, 7 | BaseDataSource, 8 | Resolver, 9 | SchemaFile, 10 | GraphqlApiBase, 11 | DataSourceOptions, 12 | ExtendedResolverProps, 13 | HttpDataSourceOptions, 14 | GraphqlApiProps, 15 | CfnDataSource, 16 | CfnFunctionConfiguration, 17 | } from 'aws-cdk-lib/aws-appsync'; 18 | import { IDomain as IOpenSearchDomain } from 'aws-cdk-lib/aws-opensearchservice'; 19 | import * as fs from 'node:fs'; 20 | import { GraphQLObjectType, isObjectType } from 'graphql'; 21 | import { makeExecutableSchema } from '@graphql-tools/schema'; 22 | import * as esbuild from 'esbuild'; 23 | import { readFileSync } from 'node:fs'; 24 | import { CfnResource } from 'aws-cdk-lib'; 25 | import { ITable } from 'aws-cdk-lib/aws-dynamodb'; 26 | import { IEventBus } from 'aws-cdk-lib/aws-events'; 27 | import { IFunction } from 'aws-cdk-lib/aws-lambda'; 28 | import { IServerlessCluster } from 'aws-cdk-lib/aws-rds'; 29 | import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; 30 | import path = require('node:path'); 31 | 32 | const SCHEMA_DEFINITIONS = ` 33 | scalar AWSTime 34 | scalar AWSDateTime 35 | scalar AWSTimestamp 36 | scalar AWSEmail 37 | scalar AWSJSON 38 | scalar AWSURL 39 | scalar AWSPhone 40 | scalar AWSIPAddress 41 | scalar BigInt 42 | scalar Double 43 | 44 | directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION 45 | 46 | # Allows transformer libraries to deprecate directive arguments. 47 | directive @deprecated(reason: String!) on INPUT_FIELD_DEFINITION | ENUM 48 | 49 | directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION 50 | directive @aws_api_key on FIELD_DEFINITION | OBJECT 51 | directive @aws_iam on FIELD_DEFINITION | OBJECT 52 | directive @aws_oidc on FIELD_DEFINITION | OBJECT 53 | directive @aws_cognito_user_pools( 54 | cognito_groups: [String!] 55 | ) on FIELD_DEFINITION | OBJECT 56 | `; 57 | 58 | const DEF_RESOLVER_CODE = ` 59 | export function request(){ return {} } 60 | export function response(ctx){ return ctx.prev.result} 61 | `.trim(); 62 | 63 | const TS_CONFIG = 64 | `{ "compilerOptions": { "target": "es2021", "module": "Node16", "noEmit": true, "moduleResolution": "node" } }`.trim(); 65 | 66 | type BaseResolver = { 67 | key: string; 68 | kind: 'UNIT' | 'PIPELINE'; 69 | typeName: string; 70 | fieldName: string; 71 | }; 72 | 73 | type UnitResolverFile = BaseResolver & { 74 | kind: 'UNIT'; 75 | dsName: string; 76 | }; 77 | 78 | type PipelineResolverGroup = BaseResolver & { 79 | kind: 'PIPELINE'; 80 | hasCode?: boolean; 81 | fns: AppSyncFn[]; 82 | }; 83 | 84 | type AppSyncFn = { 85 | name: string; 86 | description?: string; 87 | key: string; 88 | dsName: string; 89 | order: number; 90 | }; 91 | 92 | export interface AppSyncHelperProps extends Omit { 93 | name?: string; 94 | basedir: string; 95 | } 96 | 97 | export class AppSyncHelper extends GraphqlApiBase { 98 | private basedir: string; 99 | private bindCalled = false; 100 | 101 | public readonly api: GraphqlApi; 102 | 103 | /** 104 | * a map of datasource names to datasources. 105 | */ 106 | private readonly datasources: Record = {}; 107 | 108 | /** 109 | * a map of resolver names to resolvers. 110 | */ 111 | public readonly resolvers: Record = {}; 112 | 113 | /** 114 | * a map of resolver names to a sparse array of functions. 115 | */ 116 | public readonly functions: Record = {}; 117 | 118 | constructor(scope: Construct, id: string, props: AppSyncHelperProps) { 119 | super(scope, id); 120 | 121 | const { basedir, name: propsName, ...rest } = props; 122 | 123 | const name = propsName ?? id; 124 | this.basedir = basedir; 125 | const apiId = path.basename(basedir); 126 | this.api = new GraphqlApi(this, apiId, { 127 | name, 128 | ...rest, 129 | schema: SchemaFile.fromAsset(path.join(basedir, 'schema.graphql')), 130 | }); 131 | } 132 | 133 | public get apiId() { 134 | return this.api.apiId; 135 | } 136 | public get arn() { 137 | return this.api.arn; 138 | } 139 | 140 | private connect(datasource: BaseDataSource) { 141 | this.datasources[datasource.name] = datasource; 142 | } 143 | 144 | public addCfnDataSource(ds: CfnDataSource) { 145 | const datasource = { 146 | name: ds.name, 147 | ds, 148 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 149 | createResolver: (id: string, props: unknown) => { 150 | throw new Error('not implemented'); 151 | return; 152 | }, 153 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 154 | createFunction: (id: string, props: unknown) => { 155 | throw new Error('not implemented'); 156 | return; 157 | }, 158 | } as BaseDataSource; 159 | this.connect(datasource); 160 | return datasource; 161 | } 162 | 163 | public bind() { 164 | if (this.bindCalled) { 165 | throw new Error('bind() already called'); 166 | } 167 | const schemaStr = readFileSync(path.join(this.basedir, 'schema.graphql'), 'utf-8'); 168 | const typeDefs = SCHEMA_DEFINITIONS + schemaStr; 169 | const schema = makeExecutableSchema({ typeDefs }); 170 | const types = schema.getTypeMap(); 171 | 172 | const resolverFiles = this.getUnitResolvers(); 173 | const pipelines = this.getPipelineResolvers(); 174 | 175 | for (const resolverFile of [...resolverFiles, ...pipelines]) { 176 | // make sure the type exists on the schema 177 | const type = types[resolverFile.typeName]; 178 | if (!type || !isObjectType(type)) { 179 | throw new Error(`Type ${resolverFile.typeName} not defined in schema`); 180 | } 181 | 182 | // make sure the field exists on the type 183 | const fields = (type as GraphQLObjectType).getFields(); 184 | const field = fields[resolverFile.fieldName]; 185 | if (!field) { 186 | throw new Error( 187 | `Field ${resolverFile.fieldName} not defined on type ${resolverFile.typeName}` 188 | ); 189 | } 190 | 191 | if (resolverFile.kind === 'UNIT') { 192 | this.buildResolver(resolverFile); 193 | } else { 194 | this.buildPieplineResolver(resolverFile); 195 | } 196 | } 197 | this.bindCalled = true; 198 | } 199 | 200 | private build(key: string) { 201 | const result = esbuild.buildSync({ 202 | bundle: true, 203 | write: false, 204 | outdir: path.dirname(key), 205 | // outbase: path.dirname(fn.key), 206 | entryPoints: [key], 207 | format: 'esm', 208 | platform: 'node', 209 | target: 'node16', 210 | sourcemap: 'inline', 211 | sourcesContent: false, 212 | tsconfigRaw: TS_CONFIG, 213 | external: ['@aws-appsync/utils'], 214 | }); 215 | if (result.errors.length) { 216 | throw new Error('Could not build' + key + ': ' + result.errors.join('\n')); 217 | } 218 | fs.writeFileSync(result.outputFiles[0].path, result.outputFiles[0].text); 219 | return result.outputFiles[0]; 220 | } 221 | 222 | private buildPieplineResolver(resolverFile: PipelineResolverGroup) { 223 | const { typeName, fieldName } = resolverFile; 224 | const fns: { order: number; fn: AppsyncFunction }[] = []; 225 | const resId = `${typeName}_${fieldName}`; 226 | 227 | for (const fn of resolverFile.fns) { 228 | const dataSource = this.datasources[fn.dsName]; 229 | if (!dataSource) { 230 | throw new Error('datasource undefined: ' + fn.dsName); 231 | } 232 | 233 | const buildResult = this.build(fn.key); 234 | const fnId = `FN_${typeName}_${fieldName}_${fn.order}`; 235 | const fnR = new AppsyncFunction(this.api, fnId, { 236 | api: this.api, 237 | name: fnId, 238 | description: fn.description, 239 | dataSource, 240 | code: Code.fromInline(buildResult.text), 241 | runtime: FunctionRuntime.JS_1_0_0, 242 | }); 243 | // make sure function version is not set 244 | delete (fnR.node.defaultChild as CfnFunctionConfiguration)?.functionVersion; 245 | fns.push({ order: fn.order, fn: fnR }); 246 | } 247 | const pipelineConfig = fns.sort((a, b) => a.order - b.order).map((obj) => obj.fn); 248 | const max = fns[fns.length - 1].order; 249 | this.functions[resId] = new Array(max + 1); 250 | fns.forEach((fn) => (this.functions[resId][fn.order] = fn.fn)); 251 | 252 | let code: Code; 253 | if (resolverFile.hasCode) { 254 | const buildResult = this.build(path.join(resolverFile.key, 'index.ts')); 255 | code = Code.fromInline(buildResult.text); 256 | } else { 257 | code = Code.fromInline(DEF_RESOLVER_CODE); 258 | } 259 | 260 | const resolver = new Resolver(this.api, resId, { 261 | api: this.api, 262 | typeName, 263 | fieldName, 264 | code, 265 | runtime: FunctionRuntime.JS_1_0_0, 266 | pipelineConfig, 267 | }); 268 | 269 | this.resolvers[resId] = resolver; 270 | } 271 | 272 | private buildResolver(resolverFile: UnitResolverFile) { 273 | const { typeName, fieldName } = resolverFile; 274 | const dataSource = this.datasources[resolverFile.dsName]; 275 | if (!dataSource) { 276 | throw new Error('datasource undefined: ' + resolverFile.dsName); 277 | } 278 | 279 | const buildResult = this.build(resolverFile.key); 280 | const resId = `${typeName}_${fieldName}`; 281 | const resolver = new Resolver(this.api, resId, { 282 | api: this.api, 283 | dataSource, 284 | typeName, 285 | fieldName, 286 | code: Code.fromInline(buildResult.text), 287 | runtime: FunctionRuntime.JS_1_0_0, 288 | }); 289 | this.resolvers[resId] = resolver; 290 | } 291 | 292 | /** 293 | * Finds available resolvers files in the resolvers folder 294 | * @returns a list of resolver locations 295 | */ 296 | getUnitResolvers() { 297 | const resolverPath = path.join(this.basedir, 'resolvers'); 298 | if (!fs.existsSync(resolverPath)) { 299 | // noop 300 | return []; 301 | } 302 | let folders: fs.Dirent[] = []; 303 | folders = fs.readdirSync(resolverPath, { withFileTypes: true }); 304 | 305 | const RES_LOC_REG = /^(?\w+)\.(?\w+)\.\[(?\w+)\]\.(ts)$/; 306 | 307 | // find all the resolvers and create a single function pipeline resolver 308 | const resolvers = 309 | folders 310 | .filter((d) => d.isFile) 311 | .filter((f) => f.name.match(RES_LOC_REG)) 312 | .map((d) => { 313 | const name = d.name; 314 | const m = name.match(RES_LOC_REG)!; 315 | const key: string = path.join(this.basedir, 'resolvers', name); 316 | return { 317 | key, 318 | kind: 'UNIT', 319 | dsName: m.groups!.ds, 320 | typeName: m.groups!.typeName, 321 | fieldName: m.groups!.fieldName, 322 | }; 323 | }) ?? []; 324 | // resolverMappings.push(...directMappings) 325 | // return resolverMappings 326 | return resolvers; 327 | } 328 | 329 | /** 330 | * Finds pipeline resolvers files in the resolvers folder 331 | * @returns a list of resolver locations 332 | */ 333 | getPipelineResolvers() { 334 | const resolverPath = path.join(this.basedir, 'resolvers'); 335 | if (!fs.existsSync(resolverPath)) { 336 | // noop 337 | return []; 338 | } 339 | 340 | let folders: fs.Dirent[] = []; 341 | folders = fs.readdirSync(resolverPath, { withFileTypes: true }); 342 | 343 | const RES_DIR_REG = /^(?\w+)\.(?\w+)$/; 344 | const FN_LOC_REG = /^((?\w+)\.)?(?\d+)\.\[(?\w+)\]\.(ts)$/; 345 | 346 | // find all the resolvers and create a single function pipeline resolver 347 | const resolvers = 348 | folders 349 | .filter((entry) => entry.isDirectory()) 350 | .filter((entry) => entry.name.match(RES_DIR_REG)) 351 | .map((entry) => { 352 | const name = entry.name; 353 | const m = name.match(RES_DIR_REG)!; 354 | const key = path.join(this.basedir, 'resolvers', name); 355 | return { 356 | key, 357 | kind: 'PIPELINE', 358 | typeName: m.groups!.typeName, 359 | fieldName: m.groups!.fieldName, 360 | fns: [], 361 | }; 362 | }) ?? []; 363 | 364 | // for each pipeline function directory, find the functions 365 | for (const resolver of resolvers) { 366 | const entries = fs.readdirSync(resolver.key); 367 | 368 | // check if there is an index file for this pipeline 369 | const index = entries.filter((i) => i.match(/index\.ts/)); 370 | resolver.hasCode = index.length > 0; 371 | 372 | const fnEntries = entries.filter((i) => i.match(FN_LOC_REG)); 373 | const fns = fnEntries.map((entry) => { 374 | const m = entry.match(FN_LOC_REG)!; 375 | return { 376 | name: `${resolver.typeName}_${resolver.fieldName}_${m.groups!.order}`, 377 | description: m.groups!.description, 378 | key: path.join(resolver.key, entry), 379 | dsName: m.groups!.ds, 380 | order: parseInt(m.groups!.order), 381 | }; 382 | }); 383 | resolver.fns.push(...fns); 384 | } 385 | return resolvers; 386 | } 387 | 388 | // from base 389 | 390 | addNoneDataSource(id: string, options?: DataSourceOptions) { 391 | const ds = this.api.addNoneDataSource(id, options); 392 | this.connect(ds); 393 | return ds; 394 | } 395 | 396 | addDynamoDbDataSource(id: string, table: ITable, options?: DataSourceOptions) { 397 | const ds = this.api.addDynamoDbDataSource(id, table, options); 398 | this.connect(ds); 399 | return ds; 400 | } 401 | 402 | addHttpDataSource(id: string, endpoint: string, options?: HttpDataSourceOptions) { 403 | const ds = this.api.addHttpDataSource(id, endpoint, options); 404 | this.connect(ds); 405 | return ds; 406 | } 407 | addLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions) { 408 | const ds = this.api.addLambdaDataSource(id, lambdaFunction, options); 409 | this.connect(ds); 410 | return ds; 411 | } 412 | addRdsDataSource( 413 | id: string, 414 | serverlessCluster: IServerlessCluster, 415 | secretStore: ISecret, 416 | databaseName?: string, 417 | options?: DataSourceOptions 418 | ) { 419 | const ds = this.api.addRdsDataSource(id, serverlessCluster, secretStore, databaseName, options); 420 | this.connect(ds); 421 | return ds; 422 | } 423 | 424 | addEventBridgeDataSource(id: string, eventBus: IEventBus, options?: DataSourceOptions) { 425 | const ds = this.api.addEventBridgeDataSource(id, eventBus, options); 426 | this.connect(ds); 427 | return ds; 428 | } 429 | addOpenSearchDataSource(id: string, domain: IOpenSearchDomain, options?: DataSourceOptions) { 430 | const ds = this.api.addOpenSearchDataSource(id, domain, options); 431 | this.connect(ds); 432 | return ds; 433 | } 434 | createResolver(id: string, props: ExtendedResolverProps) { 435 | return this.api.createResolver(id, props); 436 | } 437 | addSchemaDependency(construct: CfnResource) { 438 | return this.api.addSchemaDependency(construct); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-helper", 3 | "version": "0.1.0", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "test": "jest", 10 | "init": "$npm_execpath install; $npm_execpath run build; $npm_execpath link" 11 | }, 12 | "devDependencies": { 13 | "@types/jest": "^29.5.1", 14 | "@types/node": "20.1.7", 15 | "aws-cdk-lib": "2.87.0", 16 | "constructs": "^10.0.0", 17 | "jest": "^29.5.0", 18 | "ts-jest": "^29.1.0", 19 | "typescript": "~5.1.3", 20 | "@graphql-tools/schema": "^10.0.0", 21 | "esbuild": "^0.18.17", 22 | "graphql": "^16.7.1" 23 | }, 24 | "peerDependencies": { 25 | "aws-cdk-lib": "2.87.0", 26 | "constructs": "^10.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/test/appsync-helper.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as AppsyncHelper from '../lib/index'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/index.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // const stack = new cdk.Stack(app, "TestStack"); 10 | // // WHEN 11 | // new AppsyncHelper.AppsyncHelper(stack, 'MyTestConstruct'); 12 | // // THEN 13 | // const template = Template.fromStack(stack); 14 | 15 | // template.hasResourceProperties('AWS::SQS::Queue', { 16 | // VisibilityTimeout: 300 17 | // }); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/cdk/constructs/appsync-helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": {}, 14 | "overrides": [ 15 | { 16 | "files": ["lib/appsync/resolvers/*.ts"], 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:@aws-appsync/recommended" 21 | ], 22 | "parserOptions": { 23 | "ecmaVersion": "latest", 24 | "sourceType": "module", 25 | "project": "./tsconfig.json" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | **/codegen/graphql 11 | **/codegen/API.ts 12 | output.json -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/README.md: -------------------------------------------------------------------------------- 1 | # Todo API - CDK Example 2 | 3 | This CDK app provides an implementation for a simple Todo API with AppSync JavaScript resolvers. 4 | 5 | The API allows you to interact with a Todo type that is backed by a DynamoDB table. 6 | 7 | ```graphql 8 | type Todo { 9 | id: ID! 10 | title: String 11 | description: String 12 | owner: String 13 | } 14 | ``` 15 | 16 | The app uses the [AppSync Helper construct](../constructs/appsync-helper/) for a quick setup. 17 | 18 | ```sh 19 | lib 20 | ├── appsync 21 | │ ├── codegen 22 | │ │ └── index.ts 23 | │ ├── resolvers 24 | │ │ ├── Mutation.createTodo.[todos].ts 25 | │ │ ├── Mutation.deleteTodo.[todos].ts 26 | │ │ ├── Mutation.updateTodo.[todos].ts 27 | │ │ ├── Query.getTodo.[todos].ts 28 | │ │ ├── Query.listTodos.[todos].ts 29 | │ │ └── utils.ts 30 | │ └── schema.graphql 31 | ├── appsync-helper.ts 32 | └── dynamodb-todo-app-stack.ts 33 | ``` 34 | 35 | The [appsync](./lib/appsync) folder contains schema and the resolver code. This project uses unit resolvers, but you can expand it to use pipeline resolvers if needed. 36 | 37 | ## Pre-req 38 | 39 | Build the `appsync-helper`. See the [README](../constructs/appsync-helper/README.md#init). 40 | 41 | ## Init 42 | 43 | Install all the dependencies. From the [project folder](./) 44 | 45 | ```sh 46 | npm install 47 | ``` 48 | 49 | ## Codegen 50 | 51 | This TypeScript projects uses types generated by [Amplify Codegen](https://docs.amplify.aws/cli-legacy/graphql-transformer/codegen/). To populate your codegen and generate it after you update your schema, run the command: 52 | 53 | ```sh 54 | npm run codegen 55 | ``` 56 | 57 | ## Deploy the stack 58 | 59 | To deploy the stack, from the top folder: 60 | 61 | ```sh 62 | npm run cdk deploy -- -O output.json 63 | ``` 64 | 65 | Once deployed, you can find your API **TodoAPI** in the AWS console. 66 | 67 | ## Test the API 68 | 69 | From the "Queries Editor" in the AWS console, create a new todo: 70 | 71 | ```graphql 72 | mutation NewTodo { 73 | createTodo(input: {description: "Hello World!", 74 | owner: "", 75 | title: "my first task"}) { 76 | description 77 | id 78 | owner 79 | title 80 | } 81 | } 82 | ``` 83 | 84 | Copy the `id` that's returned. You can now update your todo: 85 | 86 | ```graphql 87 | mutation NewTodo { 88 | updateTodo(input: {id: "", description: "Bonjour!"}) { 89 | description 90 | id 91 | owner 92 | title 93 | } 94 | } 95 | ``` 96 | 97 | If you try to update an item that does not exist, you get an error message. You can now get, list, and even subscribe to todos! 98 | 99 | ## Connect to an app 100 | 101 | You can use the info in [./output.json](./output.json) to configure your Amplify client in your app. See [Building a client application](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app.html). 102 | 103 | ## Destroy the stack 104 | 105 | To destroy the stack and all the created resources: 106 | 107 | ```sh 108 | npm run cdk destroy 109 | ``` 110 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/bin/dynamodb-todo-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { DynamodbTodoAppStack } from '../lib/dynamodb-todo-app-stack'; 5 | 6 | const app = new cdk.App(); 7 | new DynamodbTodoAppStack(app, 'DynamodbTodoAppStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/dynamodb-todo-app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | Codegen Project: 3 | schemaPath: schema.graphql 4 | includes: 5 | - codegen/graphql/**/*.ts 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: typescript 11 | generatedFileName: codegen/API.ts 12 | docsFilePath: codegen/graphql 13 | region: us-east-1 14 | apiId: null 15 | frontend: javascript 16 | framework: none 17 | maxDepth: 2 18 | extensions: 19 | amplify: 20 | version: 3 21 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/codegen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './API'; 2 | export type Result = Omit; 3 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/Mutation.createTodo.[todos].ts: -------------------------------------------------------------------------------- 1 | import { util, Context } from '@aws-appsync/utils'; 2 | import { CreateTodoMutationVariables } from '../codegen'; 3 | import { dynamodbPutRequest } from './utils'; 4 | export { checkErrorsAndRespond as response } from './utils'; 5 | 6 | export function request(ctx: Context) { 7 | const { input: values } = ctx.arguments; 8 | const key = { id: util.autoId() }; 9 | const condition = { id: { attributeExists: false } }; 10 | console.log('--> create todo with requested values: ', values); 11 | return dynamodbPutRequest({ key, values, condition }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/Mutation.deleteTodo.[todos].ts: -------------------------------------------------------------------------------- 1 | import { util, Context } from '@aws-appsync/utils'; 2 | import { DeleteTodoMutationVariables } from '../codegen'; 3 | export { checkErrorsAndRespond as response } from './utils'; 4 | 5 | export function request(ctx: Context) { 6 | const { input: key } = ctx.arguments; 7 | return { 8 | operation: 'DeleteItem', 9 | key: util.dynamodb.toMapValues(key), 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/Mutation.updateTodo.[todos].ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@aws-appsync/utils'; 2 | import { UpdateTodoMutationVariables } from '../codegen'; 3 | import { dynamodbUpdateRequest } from './utils'; 4 | export { checkErrorsAndRespond as response } from './utils'; 5 | 6 | export function request(ctx: Context) { 7 | const {id, ...values} = ctx.args.input; 8 | const condition = { id: { attributeExists: true } }; 9 | console.log('--> update todo with requested values: ', values); 10 | return dynamodbUpdateRequest({ key: {id}, values, condition }); 11 | } 12 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/Query.getTodo.[todos].ts: -------------------------------------------------------------------------------- 1 | import { util, Context } from '@aws-appsync/utils'; 2 | import { GetTodoQueryVariables } from '../codegen'; 3 | 4 | export function request(ctx: Context) { 5 | return { 6 | operation: 'GetItem', 7 | key: util.dynamodb.toMapValues({ id: ctx.args.id }), 8 | }; 9 | } 10 | 11 | export const response = (ctx: Context) => ctx.result; 12 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/Query.listTodos.[todos].ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@aws-appsync/utils'; 2 | import { Result, ListTodosQuery, ListTodosQueryVariables } from '../codegen'; 3 | 4 | export function request(ctx: Context) { 5 | const { limit = 10, nextToken } = ctx.args; 6 | return { operation: 'Scan', limit, nextToken }; 7 | } 8 | 9 | export function response(ctx: Context): Result { 10 | const { items = [], nextToken } = ctx.result; 11 | return { items, nextToken }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/resolvers/utils.ts: -------------------------------------------------------------------------------- 1 | import { Context, DynamoDBPutItemRequest, Key } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Checks for errors and returns the `result 5 | */ 6 | export function checkErrorsAndRespond(ctx: Context) { 7 | if (ctx.error) { 8 | util.error(ctx.error.message, ctx.error.type); 9 | } 10 | return ctx.result; 11 | } 12 | 13 | type PutRequestType = { 14 | key: Key; 15 | values: Record; 16 | condition: Record; 17 | }; 18 | /** 19 | * Helper function to create a new item 20 | * @returns a PutItem request 21 | */ 22 | export function dynamodbPutRequest({ 23 | key, 24 | values, 25 | condition: inCondObj, 26 | }: PutRequestType): DynamoDBPutItemRequest { 27 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 28 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 29 | delete condition.expressionValues; 30 | } 31 | return { 32 | operation: 'PutItem', 33 | key: util.dynamodb.toMapValues(key), 34 | attributeValues: util.dynamodb.toMapValues(values), 35 | condition, 36 | }; 37 | } 38 | 39 | type UpdateRequestType = { 40 | key: Key; 41 | values: Record; 42 | condition: Record; 43 | }; 44 | export function dynamodbUpdateRequest({ key, values, condition: inCondObj }: UpdateRequestType) { 45 | const sets: string[] = []; 46 | const removes: string[] = []; 47 | const expressionNames: Record = {}; 48 | const expValues: Record = {}; 49 | 50 | for (const [k, v] of Object.entries(values)) { 51 | expressionNames[`#${k}`] = k; 52 | if (v) { 53 | sets.push(`#${k} = :${k}`); 54 | expValues[`:${k}`] = v; 55 | } else { 56 | removes.push(`#${k}`); 57 | } 58 | } 59 | 60 | console.log(`SET: ${sets.length}, REMOVE: ${removes.length}`); 61 | 62 | let expression = sets.length ? `SET ${sets.join(', ')}` : ''; 63 | expression += removes.length ? ` REMOVE ${removes.join(', ')}` : ''; 64 | 65 | console.log('update expression', expression); 66 | 67 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 68 | 69 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 70 | delete condition.expressionValues; 71 | } 72 | 73 | return { 74 | operation: 'UpdateItem', 75 | key: util.dynamodb.toMapValues(key), 76 | condition, 77 | update: { 78 | expression, 79 | expressionNames, 80 | expressionValues: util.dynamodb.toMapValues(expValues), 81 | }, 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/appsync/schema.graphql: -------------------------------------------------------------------------------- 1 | type Todo { 2 | id: ID! 3 | title: String 4 | description: String 5 | owner: String 6 | } 7 | 8 | type Mutation { 9 | createTodo(input: CreateTodoInput!): Todo 10 | updateTodo(input: UpdateTodoInput!): Todo 11 | deleteTodo(input: DeleteTodoInput!): Todo 12 | } 13 | 14 | type Query { 15 | getTodo(id: ID!): Todo 16 | listTodos(filter: TableTodoFilterInput, limit: Int, nextToken: String): TodoConnection 17 | } 18 | 19 | type Subscription { 20 | onCreateTodo(id: ID, title: String, description: String, owner: String): Todo 21 | @aws_subscribe(mutations: ["createTodo"]) 22 | onUpdateTodo(id: ID, title: String, description: String, owner: String): Todo 23 | @aws_subscribe(mutations: ["updateTodo"]) 24 | onDeleteTodo(id: ID, title: String, description: String, owner: String): Todo 25 | @aws_subscribe(mutations: ["deleteTodo"]) 26 | } 27 | 28 | input CreateTodoInput { 29 | title: String 30 | description: String 31 | owner: String 32 | } 33 | 34 | input DeleteTodoInput { 35 | id: ID! 36 | } 37 | 38 | input TableBooleanFilterInput { 39 | ne: Boolean 40 | eq: Boolean 41 | } 42 | 43 | input TableFloatFilterInput { 44 | ne: Float 45 | eq: Float 46 | le: Float 47 | lt: Float 48 | ge: Float 49 | gt: Float 50 | contains: Float 51 | notContains: Float 52 | between: [Float] 53 | } 54 | 55 | input TableIDFilterInput { 56 | ne: ID 57 | eq: ID 58 | le: ID 59 | lt: ID 60 | ge: ID 61 | gt: ID 62 | contains: ID 63 | notContains: ID 64 | between: [ID] 65 | beginsWith: ID 66 | } 67 | 68 | input TableIntFilterInput { 69 | ne: Int 70 | eq: Int 71 | le: Int 72 | lt: Int 73 | ge: Int 74 | gt: Int 75 | contains: Int 76 | notContains: Int 77 | between: [Int] 78 | } 79 | 80 | input TableStringFilterInput { 81 | ne: String 82 | eq: String 83 | le: String 84 | lt: String 85 | ge: String 86 | gt: String 87 | contains: String 88 | notContains: String 89 | between: [String] 90 | beginsWith: String 91 | } 92 | 93 | input TableTodoFilterInput { 94 | id: TableIDFilterInput 95 | title: TableStringFilterInput 96 | description: TableStringFilterInput 97 | owner: TableStringFilterInput 98 | } 99 | 100 | type TodoConnection { 101 | items: [Todo] 102 | nextToken: String 103 | } 104 | 105 | input UpdateTodoInput { 106 | id: ID! 107 | title: String 108 | description: String 109 | owner: String 110 | } 111 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/lib/dynamodb-todo-app-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { AppSyncHelper } from 'appsync-helper'; 4 | import path = require('node:path'); 5 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 6 | import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; 7 | 8 | export class DynamodbTodoAppStack extends cdk.Stack { 9 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const todoTable = new dynamodb.Table(this, 'TodoTable', { 13 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 14 | }); 15 | const appSyncApp = new AppSyncHelper(this, 'TodoAPI', { 16 | basedir: path.join(__dirname, 'appsync'), 17 | logConfig: { 18 | fieldLogLevel: FieldLogLevel.ALL, 19 | excludeVerboseContent: false, 20 | retention: cdk.aws_logs.RetentionDays.ONE_WEEK, 21 | }, 22 | xrayEnabled: true, 23 | }); 24 | appSyncApp.addDynamoDbDataSource('todos', todoTable); 25 | appSyncApp.bind(); 26 | 27 | new cdk.CfnOutput(this, 'GRAPHQLENDPOINT', { value: appSyncApp.api.graphqlUrl }); 28 | new cdk.CfnOutput(this, 'REGION', { value: cdk.Stack.of(this).region }); 29 | new cdk.CfnOutput(this, 'AUTHTYPE', { value: 'API_KEY' }); 30 | new cdk.CfnOutput(this, 'APIKEY', { value: appSyncApp.api.apiKey! }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-todo-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "dynamodb-todo-app": "bin/dynamodb-todo-app.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "prebuild": "$npm_execpath run codegen", 13 | "codegen": "cd lib/appsync && npx --package=@aws-amplify/cli amplify codegen" 14 | }, 15 | "devDependencies": { 16 | "@aws-appsync/eslint-plugin": "^1.2.6", 17 | "@aws-appsync/utils": "1.2.6", 18 | "@graphql-tools/schema": "^10.0.0", 19 | "@types/jest": "^29.5.1", 20 | "@types/node": "20.1.7", 21 | "@typescript-eslint/eslint-plugin": "^6.3.0", 22 | "@typescript-eslint/parser": "^6.3.0", 23 | "appsync-helper": "file:../constructs/appsync-helper", 24 | "aws-cdk": "2.87.0", 25 | "esbuild": "^0.18.17", 26 | "eslint": "^8.46.0", 27 | "graphql": "^16.7.1", 28 | "jest": "^29.5.0", 29 | "ts-jest": "^29.1.0", 30 | "ts-node": "^10.9.1", 31 | "typescript": "~5.1.3" 32 | }, 33 | "dependencies": { 34 | "aws-cdk-lib": "2.87.0", 35 | "constructs": "^10.0.0", 36 | "source-map-support": "^0.5.21" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/test/dynamodb-todo-app.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as DynamodbTodoApp from '../lib/dynamodb-todo-app-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/dynamodb-todo-app-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new DynamodbTodoApp.DynamodbTodoAppStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/cdk/dynamodb-todo-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020", "dom"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/.eslintignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": {}, 14 | "overrides": [ 15 | { 16 | "files": ["lib/appsync/resolvers/*.ts"], 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:@aws-appsync/recommended" 21 | ], 22 | "parserOptions": { 23 | "ecmaVersion": "latest", 24 | "sourceType": "module", 25 | "project": "./tsconfig.json" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | **/codegen/graphql 11 | **/codegen/API.ts 12 | output.json -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/README.md: -------------------------------------------------------------------------------- 1 | # Local PubSub API - CDK Example 2 | 3 | This CDK app provides an implementation of an AppSync API that can be used to implement a simple pub sub API for a chat system. The API uses a local NONE resolver. This means that no data is saved. When a message is published to the system, it is immediately made available to clients that have subscribed to receive message. The system works with anonymous users and authenticated users. 4 | 5 | The core of the API is built around the `Message` type 6 | 7 | ```graphql 8 | type Message @aws_api_key @aws_cognito_user_pools { 9 | id: ID! 10 | text: String! 11 | from: String! 12 | kind: MSG_KIND! 13 | to: String 14 | createdAt: AWSDateTime 15 | } 16 | 17 | ``` 18 | 19 | The app uses the [AppSync Helper construct](../constructs/appsync-helper/) for a quick setup. 20 | 21 | ```sh 22 | lib 23 | ├── appsync 24 | │ ├── codegen 25 | │ │ ├── graphql 26 | │ │ │ ├── mutations.ts 27 | │ │ │ ├── queries.ts 28 | │ │ │ └── subscriptions.ts 29 | │ │ ├── API.ts 30 | │ │ └── index.ts 31 | │ ├── resolvers 32 | │ │ ├── Mutation.publish.[NONE].ts 33 | │ │ ├── Subscription.onPublish.[NONE].ts 34 | │ │ └── utils.d.ts 35 | │ └── schema.graphql 36 | ├── appsync-helper.ts 37 | └── pub-sub-app-stack.ts 38 | ``` 39 | 40 | The [appsync](./lib/appsync/) folder contains schema and the resolver code. This project uses unit resolvers, but you can expand it to use pipeline resolvers if needed. 41 | 42 | ## Pre-req 43 | 44 | Build the `appsync-helper`. See the [README](../constructs/appsync-helper/README.md#init). 45 | 46 | ## Init 47 | 48 | Install all the dependencies. From the [project folder](./) 49 | 50 | ```sh 51 | npm install 52 | ``` 53 | 54 | ## Codegen 55 | 56 | This TypeScript projects uses types generated by [Amplify Codegen](https://docs.amplify.aws/cli-legacy/graphql-transformer/codegen/). To populate your codegen and generate it after you update your schema, run the command: 57 | 58 | ```sh 59 | npm run codegen 60 | ``` 61 | 62 | ## Deploy the stack 63 | 64 | To deploy the stack, from the top folder: 65 | 66 | ```sh 67 | npm run cdk deploy -- -O output.json 68 | ``` 69 | 70 | Once deployed, you can find your API **LocalMessageAPI** in the AWS AppSync console. 71 | 72 | ## Testing the API 73 | 74 | In the AppSync console, head to the **Queries Editor**. Using the 'API Key' authorization mode, set up a subscription: 75 | 76 | ```graphql 77 | subscription Sub { 78 | onPublish { 79 | createdAt 80 | from 81 | id 82 | kind 83 | text 84 | to 85 | } 86 | } 87 | ``` 88 | 89 | In a separate browser tab or window, navigate to the **Queries Editor** page again and send this mutation: 90 | 91 | ```graphql 92 | mutation Pub { 93 | publish(input: {kind: ALL, text: "Hello World!"}) { 94 | createdAt 95 | from 96 | id 97 | kind 98 | text 99 | to 100 | } 101 | } 102 | ``` 103 | 104 | In your first tab, you receive the message. Next, try it out as a signed in cognito user. You can head to the Amazon Cognito console to set up a user or sign up a user named `tester` with these commands: 105 | 106 | ```sh 107 | CLIENT_ID="" 108 | EMAIL="" 109 | aws cognito-idp sign-up --client-id $CLIENT_ID --username tester --password TempPassword1234! --user-attributes Name=email,Value=$EMAIL 110 | ``` 111 | 112 | You will receive a confirmation code at your email address. Then confirm your sign-up: 113 | 114 | ```sh 115 | aws cognito-idp confirm-sign-up --client-id $CLIENT_ID --username tester --confirmation-code 116 | ``` 117 | 118 | In the tab where you sent the mutation, log in as the `tester` user. Then send this message to yourself: 119 | 120 | ```graphql 121 | mutation Pub { 122 | publish(input: {kind: DIRECT, text: "hello", to: "tester"}) { 123 | createdAt 124 | from 125 | id 126 | kind 127 | text 128 | to 129 | } 130 | } 131 | ``` 132 | 133 | In the subscription tab, you do not see the message. The subscription uses enhanced filtering and only allows anonymous users (those using the API KEY) to receive message sent to ALL. Stop your subscription and change authorization mode to Cognito User Pools. Sign in if needed, and restart the subscription. Send yourself another message, which you then receive. 134 | 135 | Try this again by creating other users and sending DIRECT or ALL messages to the various users so see that DIRECT messages are only received by the users they are addressed to. 136 | 137 | ## Connect to an app 138 | 139 | You can use the info in [./output.json](./output.json) to configure your Amplify client in your app. See [Building a client application](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app.html). 140 | 141 | ## Destroy the stack 142 | 143 | To destroy the stack and all the created resources: 144 | 145 | ```sh 146 | npm run cdk destroy 147 | ``` 148 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/bin/pub-sub-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { PubSubAppStack } from '../lib/pub-sub-app-stack'; 5 | 6 | const app = new cdk.App(); 7 | new PubSubAppStack(app, 'PubSubAppStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/pub-sub-app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | Codegen Project: 3 | schemaPath: schema.graphql 4 | includes: 5 | - codegen/graphql/**/*.ts 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: typescript 11 | generatedFileName: codegen/API.ts 12 | docsFilePath: codegen/graphql 13 | region: us-east-1 14 | apiId: null 15 | frontend: javascript 16 | framework: none 17 | maxDepth: 2 18 | extensions: 19 | amplify: 20 | version: 3 21 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/codegen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './API'; 2 | export type Result = Omit; 3 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/resolvers/Mutation.publish.[NONE].ts: -------------------------------------------------------------------------------- 1 | import { util, Context, NONERequest, AppSyncIdentityCognito } from '@aws-appsync/utils'; 2 | import { MSG_KIND, Message, PublishMutationVariables, Result } from '../codegen'; 3 | 4 | export function request(ctx: Context): NONERequest { 5 | const { text, kind, to } = ctx.args.input; 6 | let from = 'anonymous'; 7 | 8 | if (util.authType() === 'User Pool Authorization') { 9 | from = (ctx.identity as AppSyncIdentityCognito).username; 10 | } else { 11 | if (kind === MSG_KIND.DIRECT) { 12 | util.error('Anonymous user cannot send direct messages', 'ApplicationError'); 13 | } 14 | } 15 | 16 | if (kind === MSG_KIND.ALL && to) { 17 | util.error(`Cannot specify 'to' in an 'ALL' message`, 'ApplicationError'); 18 | } 19 | const msg: Result = { 20 | id: util.autoId(), 21 | createdAt: util.time.nowISO8601(), 22 | kind, 23 | text, 24 | from, 25 | to, 26 | }; 27 | 28 | return { payload: msg }; 29 | } 30 | 31 | export const response = (ctx: Context) => ctx.result; 32 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/resolvers/Query.whoami.[NONE].ts: -------------------------------------------------------------------------------- 1 | import { util, Context, NONERequest, AppSyncIdentityCognito } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx: Context): NONERequest { 4 | let me = 'anonymous'; 5 | 6 | if (util.authType() === 'User Pool Authorization') { 7 | me = (ctx.identity as AppSyncIdentityCognito).username; 8 | } 9 | 10 | return { payload: me }; 11 | } 12 | 13 | export const response = (ctx: Context) => ctx.result; 14 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/resolvers/Subscription.onPublish.[NONE].ts: -------------------------------------------------------------------------------- 1 | import { util, Context, extensions, AppSyncIdentityCognito, NONERequest } from '@aws-appsync/utils'; 2 | import { MSG_KIND, Message } from '../codegen'; 3 | 4 | export function request(ctx: Context): NONERequest { 5 | if (util.authType() === 'User Pool Authorization') { 6 | const identity = ctx.identity as AppSyncIdentityCognito; 7 | const filter = util.transform.toSubscriptionFilter({ 8 | or: [{ to: { eq: identity.username } }, { kind: { eq: MSG_KIND.ALL } }], 9 | }); 10 | extensions.setSubscriptionFilter(filter); 11 | } else { 12 | const filter = util.transform.toSubscriptionFilter({ 13 | kind: { eq: MSG_KIND.ALL }, 14 | }); 15 | extensions.setSubscriptionFilter(filter); 16 | } 17 | return { payload: null }; 18 | } 19 | 20 | export const response = (ctx: Context) => ctx.result; 21 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/appsync/schema.graphql: -------------------------------------------------------------------------------- 1 | type Message @aws_api_key @aws_cognito_user_pools { 2 | id: ID! 3 | text: String! 4 | from: String! 5 | kind: MSG_KIND! 6 | to: String 7 | createdAt: AWSDateTime 8 | } 9 | 10 | enum MSG_KIND { 11 | ALL 12 | DIRECT 13 | } 14 | 15 | type Mutation @aws_api_key @aws_cognito_user_pools { 16 | publish(input: PublishMessageInput!): Message 17 | } 18 | 19 | type Query @aws_api_key @aws_cognito_user_pools { 20 | whoami: String 21 | } 22 | 23 | type Subscription @aws_api_key @aws_cognito_user_pools { 24 | onPublish: Message @aws_subscribe(mutations: ["publish"]) 25 | } 26 | 27 | input PublishMessageInput { 28 | text: String! 29 | kind: MSG_KIND! 30 | to: String 31 | } 32 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/lib/pub-sub-app-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 3 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 4 | import { Construct } from 'constructs'; 5 | import path = require('path'); 6 | import { AppSyncHelper } from 'appsync-helper'; 7 | 8 | export class PubSubAppStack extends cdk.Stack { 9 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const userPool = new cognito.UserPool(this, 'UserPool', { 13 | userPoolName: 'PubSubAppUserPool', 14 | selfSignUpEnabled: true, 15 | autoVerify: { email: true }, 16 | standardAttributes: { email: { required: true } }, 17 | removalPolicy: cdk.RemovalPolicy.DESTROY, 18 | }); 19 | 20 | const client = userPool.addClient('customer-app-client-web', { 21 | preventUserExistenceErrors: true, 22 | authFlows: { userPassword: true, userSrp: true }, 23 | }); 24 | 25 | const appSyncApp = new AppSyncHelper(this, 'LocalMessageAPI', { 26 | basedir: path.join(__dirname, 'appsync'), 27 | authorizationConfig: { 28 | defaultAuthorization: { 29 | authorizationType: appsync.AuthorizationType.API_KEY, 30 | apiKeyConfig: { name: 'default', description: 'default auth mode' }, 31 | }, 32 | additionalAuthorizationModes: [ 33 | { 34 | authorizationType: appsync.AuthorizationType.USER_POOL, 35 | userPoolConfig: { userPool }, 36 | }, 37 | ], 38 | }, 39 | logConfig: { 40 | fieldLogLevel: appsync.FieldLogLevel.ALL, 41 | excludeVerboseContent: false, 42 | retention: cdk.aws_logs.RetentionDays.ONE_WEEK, 43 | }, 44 | xrayEnabled: true, 45 | }); 46 | appSyncApp.addNoneDataSource('NONE'); 47 | appSyncApp.bind(); 48 | 49 | new cdk.CfnOutput(this, 'GRAPHQLENDPOINT', { value: appSyncApp.api.graphqlUrl }); 50 | new cdk.CfnOutput(this, 'REGION', { value: cdk.Stack.of(this).region }); 51 | new cdk.CfnOutput(this, 'AUTHTYPE', { value: 'API_KEY' }); 52 | new cdk.CfnOutput(this, 'APIKEY', { value: appSyncApp.api.apiKey! }); 53 | new cdk.CfnOutput(this, 'USERPOOLSID', { value: userPool.userPoolId }); 54 | new cdk.CfnOutput(this, 'USERPOOLSWEBCLIENTID', { value: client.userPoolClientId }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pub-sub-app", 3 | "version": "0.1.0", 4 | "bin": { 5 | "pub-sub-app": "bin/pub-sub-app.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "prebuild": "$npm_execpath run codegen", 13 | "codegen": "cd lib/appsync && npx --package=@aws-amplify/cli amplify codegen" 14 | }, 15 | "devDependencies": { 16 | "@aws-appsync/eslint-plugin": "^1.2.6", 17 | "@aws-appsync/utils": "1.2.6", 18 | "@graphql-tools/schema": "^10.0.0", 19 | "@types/jest": "^29.5.1", 20 | "@types/node": "20.1.7", 21 | "@typescript-eslint/eslint-plugin": "^6.3.0", 22 | "@typescript-eslint/parser": "^6.3.0", 23 | "appsync-helper": "file:../constructs/appsync-helper", 24 | "aws-cdk": "2.87.0", 25 | "esbuild": "^0.18.17", 26 | "eslint": "^8.46.0", 27 | "graphql": "^16.7.1", 28 | "jest": "^29.5.0", 29 | "ts-jest": "^29.1.0", 30 | "ts-node": "^10.9.1", 31 | "typescript": "~5.1.3" 32 | }, 33 | "dependencies": { 34 | "aws-cdk-lib": "2.87.0", 35 | "constructs": "^10.0.0", 36 | "source-map-support": "^0.5.21" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/test/pub-sub-app.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as PubSubApp from '../lib/pub-sub-app-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/pub-sub-app-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new PubSubApp.PubSubAppStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/cdk/pub-sub-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020", "dom"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/README.md: -------------------------------------------------------------------------------- 1 | # Todo API 2 | 3 | This folder provides the implementation for a Todo API implemented with AppSync JavaScript resolvers. 4 | The API allows you to interact with a Todo type: 5 | 6 | ```graphql 7 | type Todo { 8 | id: ID! 9 | title: String 10 | description: String 11 | owner: String 12 | } 13 | ``` 14 | 15 | The [resolvers](./resolvers/) folder contains the code for the resolvers. 16 | 17 | ## Deploy the stack 18 | 19 | Deploy this stack by using [template.yaml](./template.yaml) from the Cloudformation console or using the AWS CLI. 20 | 21 | With the AWS CLI, from this folder: 22 | 23 | ```sh 24 | aws cloudformation deploy --template-file ./template.yaml --stack-name simple-todo-api-app --capabilities CAPABILITY_IAM 25 | ``` 26 | 27 | Once deployed, you can find your API **SimpleTodoAPI** in the AWS console. 28 | 29 | ## Delete the stack 30 | 31 | To delete your resources: visit the Cloudformation console and delete the stack. 32 | 33 | With the AWS CLI: 34 | 35 | ```sh 36 | aws cloudformation delete-stack --stack-name simple-todo-api-app 37 | ``` 38 | 39 | Note: The DynamoDB table deployed by this template is retained when the stack is deleted. 40 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/createTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | const { input: values } = ctx.arguments; 5 | const key = { id: util.autoId() }; 6 | const condition = { id: { attributeExists: false } }; 7 | console.log('--> create todo with requested values: ', values); 8 | return dynamodbPutRequest({ key, values, condition }); 9 | } 10 | 11 | export function response(ctx) { 12 | const { error, result } = ctx; 13 | if (error) { 14 | return util.appendError(error.message, error.type, result); 15 | } 16 | return ctx.result; 17 | } 18 | 19 | /** 20 | * Helper function to create a new item 21 | * @returns a PutItem request 22 | */ 23 | function dynamodbPutRequest({ key, values, condition: inCondObj }) { 24 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 25 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 26 | delete condition.expressionValues; 27 | } 28 | return { 29 | operation: 'PutItem', 30 | key: util.dynamodb.toMapValues(key), 31 | attributeValues: util.dynamodb.toMapValues(values), 32 | condition, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/deleteTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | return { 5 | operation: 'DeleteItem', 6 | key: util.dynamodb.toMapValues({ id: ctx.args.id }), 7 | }; 8 | } 9 | 10 | export function response(ctx) { 11 | const { error, result } = ctx; 12 | if (error) { 13 | return util.appendError(error.message, error.type, result); 14 | } 15 | return ctx.result; 16 | } 17 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/getTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | return { 5 | operation: 'GetItem', 6 | key: util.dynamodb.toMapValues({ id: ctx.args.id }), 7 | }; 8 | } 9 | 10 | export function response(ctx) { 11 | const { error, result } = ctx; 12 | if (error) { 13 | return util.appendError(error.message, error.type, result); 14 | } 15 | return ctx.result; 16 | } 17 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/listTodos.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | const { filter: f, limit = 20, nextToken } = ctx.args; 5 | const filter = f ? JSON.parse(util.transform.toDynamoDBFilterExpression(f)) : null; 6 | return { operation: 'Scan', filter, limit, nextToken }; 7 | } 8 | 9 | export function response(ctx) { 10 | const { error, result } = ctx; 11 | if (error) { 12 | return util.appendError(error.message, error.type, result); 13 | } 14 | const { items = [], nextToken } = result; 15 | return { items, nextToken }; 16 | } 17 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/queryTodosByOwner.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | const { owner, first = 20, after } = ctx.args; 5 | return dynamodbQueryRequest('owner', owner, 'owner-index', first, after); 6 | } 7 | 8 | export function response(ctx) { 9 | const { error, result } = ctx; 10 | if (error) { 11 | return util.appendError(error.message, error.type, result); 12 | } 13 | return result; 14 | } 15 | 16 | function dynamodbQueryRequest(key, value, index, limit, nextToken) { 17 | const expression = `#key = :key`; 18 | const expressionNames = { '#key': key }; 19 | const expressionValues = util.dynamodb.toMapValues({ ':key': value }); 20 | return { 21 | operation: 'Query', 22 | query: { expression, expressionNames, expressionValues }, 23 | index, 24 | limit, 25 | nextToken, 26 | scanIndexForward: true, 27 | select: 'ALL_ATTRIBUTES', 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/resolvers/updateTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | const { 5 | input: { id, ...values }, 6 | } = ctx.args; 7 | const condition = { id: { attributeExists: true } }; 8 | return dynamodbUpdateRequest({ key: { id }, values, condition }); 9 | } 10 | 11 | export function response(ctx) { 12 | const { error, result } = ctx; 13 | if (error) { 14 | return util.appendError(error.message, error.type, result); 15 | } 16 | return result; 17 | } 18 | 19 | function dynamodbUpdateRequest({ key, values, condition: inCondObj }) { 20 | const sets = []; 21 | const removes = []; 22 | const expressionNames = {}; 23 | const expValues = {}; 24 | 25 | for (const [k, v] of Object.entries(values)) { 26 | expressionNames[`#${k}`] = k; 27 | if (v) { 28 | sets.push(`#${k} = :${k}`); 29 | expValues[`:${k}`] = v; 30 | } else { 31 | removes.push(`#${k}`); 32 | } 33 | } 34 | 35 | console.log(`SET: ${sets.length}, REMOVE: ${removes.length}`); 36 | 37 | let expression = sets.length ? `SET ${sets.join(', ')}` : ''; 38 | expression += removes.length ? ` REMOVE ${removes.join(', ')}` : ''; 39 | 40 | console.log('update expression', expression); 41 | 42 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 43 | 44 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 45 | delete condition.expressionValues; 46 | } 47 | 48 | return { 49 | operation: 'UpdateItem', 50 | key: util.dynamodb.toMapValues(key), 51 | condition, 52 | update: { 53 | expression, 54 | expressionNames, 55 | expressionValues: util.dynamodb.toMapValues(expValues), 56 | }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/schema.graphql: -------------------------------------------------------------------------------- 1 | input CreateTodoInput { 2 | title: String 3 | description: String 4 | owner: String 5 | } 6 | 7 | input DeleteTodoInput { 8 | id: ID! 9 | } 10 | 11 | type Mutation { 12 | createTodo(input: CreateTodoInput!): Todo 13 | updateTodo(input: UpdateTodoInput!): Todo 14 | deleteTodo(input: DeleteTodoInput!): Todo 15 | } 16 | 17 | type Query { 18 | getTodo(id: ID!): Todo 19 | listTodos( 20 | filter: TableTodoFilterInput 21 | limit: Int 22 | nextToken: String 23 | ): TodoConnection 24 | queryTodosByOwnerIndex( 25 | owner: String! 26 | first: Int 27 | after: String 28 | ): TodoConnection 29 | } 30 | 31 | type Subscription { 32 | onCreateTodo(id: ID, title: String, description: String, owner: String): Todo 33 | @aws_subscribe(mutations: ["createTodo"]) 34 | onUpdateTodo(id: ID, title: String, description: String, owner: String): Todo 35 | @aws_subscribe(mutations: ["updateTodo"]) 36 | onDeleteTodo(id: ID, title: String, description: String, owner: String): Todo 37 | @aws_subscribe(mutations: ["deleteTodo"]) 38 | } 39 | 40 | input TableBooleanFilterInput { 41 | ne: Boolean 42 | eq: Boolean 43 | } 44 | 45 | input TableFloatFilterInput { 46 | ne: Float 47 | eq: Float 48 | le: Float 49 | lt: Float 50 | ge: Float 51 | gt: Float 52 | contains: Float 53 | notContains: Float 54 | between: [Float] 55 | } 56 | 57 | input TableIDFilterInput { 58 | ne: ID 59 | eq: ID 60 | le: ID 61 | lt: ID 62 | ge: ID 63 | gt: ID 64 | contains: ID 65 | notContains: ID 66 | between: [ID] 67 | beginsWith: ID 68 | } 69 | 70 | input TableIntFilterInput { 71 | ne: Int 72 | eq: Int 73 | le: Int 74 | lt: Int 75 | ge: Int 76 | gt: Int 77 | contains: Int 78 | notContains: Int 79 | between: [Int] 80 | } 81 | 82 | input TableStringFilterInput { 83 | ne: String 84 | eq: String 85 | le: String 86 | lt: String 87 | ge: String 88 | gt: String 89 | contains: String 90 | notContains: String 91 | between: [String] 92 | beginsWith: String 93 | } 94 | 95 | input TableTodoFilterInput { 96 | id: TableIDFilterInput 97 | title: TableStringFilterInput 98 | description: TableStringFilterInput 99 | owner: TableStringFilterInput 100 | } 101 | 102 | type Todo { 103 | id: ID! 104 | title: String 105 | description: String 106 | owner: String 107 | } 108 | 109 | type TodoConnection { 110 | items: [Todo] 111 | nextToken: String 112 | } 113 | 114 | input UpdateTodoInput { 115 | id: ID! 116 | title: String 117 | description: String 118 | owner: String 119 | } 120 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-cfn/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: An AppSync API with JavaScript resolvers with a DynamoDB data source 3 | Resources: 4 | TodoAPI: 5 | Type: AWS::AppSync::GraphQLApi 6 | Properties: 7 | AuthenticationType: API_KEY 8 | Name: SimpleTodoAPI 9 | TodoAPISchema: 10 | Type: AWS::AppSync::GraphQLSchema 11 | Properties: 12 | ApiId: !GetAtt TodoAPI.ApiId 13 | Definition: | 14 | input CreateTodoInput { 15 | title: String 16 | description: String 17 | owner: String 18 | } 19 | 20 | input DeleteTodoInput { 21 | id: ID! 22 | } 23 | 24 | type Mutation { 25 | createTodo(input: CreateTodoInput!): Todo 26 | updateTodo(input: UpdateTodoInput!): Todo 27 | deleteTodo(input: DeleteTodoInput!): Todo 28 | } 29 | 30 | type Query { 31 | getTodo(id: ID!): Todo 32 | listTodos(filter: TableTodoFilterInput, limit: Int, nextToken: String): TodoConnection 33 | queryTodosByOwnerIndex(owner: String!, first: Int, after: String): TodoConnection 34 | } 35 | 36 | type Subscription { 37 | onCreateTodo(id: ID, title: String, description: String, owner: String): Todo @aws_subscribe(mutations: ["createTodo"]) 38 | onUpdateTodo(id: ID, title: String, description: String, owner: String): Todo @aws_subscribe(mutations: ["updateTodo"]) 39 | onDeleteTodo(id: ID, title: String, description: String, owner: String): Todo @aws_subscribe(mutations: ["deleteTodo"]) 40 | } 41 | 42 | input TableBooleanFilterInput { 43 | ne: Boolean 44 | eq: Boolean 45 | } 46 | 47 | input TableFloatFilterInput { 48 | ne: Float 49 | eq: Float 50 | le: Float 51 | lt: Float 52 | ge: Float 53 | gt: Float 54 | contains: Float 55 | notContains: Float 56 | between: [Float] 57 | } 58 | 59 | input TableIDFilterInput { 60 | ne: ID 61 | eq: ID 62 | le: ID 63 | lt: ID 64 | ge: ID 65 | gt: ID 66 | contains: ID 67 | notContains: ID 68 | between: [ID] 69 | beginsWith: ID 70 | } 71 | 72 | input TableIntFilterInput { 73 | ne: Int 74 | eq: Int 75 | le: Int 76 | lt: Int 77 | ge: Int 78 | gt: Int 79 | contains: Int 80 | notContains: Int 81 | between: [Int] 82 | } 83 | 84 | input TableStringFilterInput { 85 | ne: String 86 | eq: String 87 | le: String 88 | lt: String 89 | ge: String 90 | gt: String 91 | contains: String 92 | notContains: String 93 | between: [String] 94 | beginsWith: String 95 | } 96 | 97 | input TableTodoFilterInput { 98 | id: TableIDFilterInput 99 | title: TableStringFilterInput 100 | description: TableStringFilterInput 101 | owner: TableStringFilterInput 102 | } 103 | 104 | type Todo { 105 | id: ID! 106 | title: String 107 | description: String 108 | owner: String 109 | } 110 | 111 | type TodoConnection { 112 | items: [Todo] 113 | nextToken: String 114 | } 115 | 116 | input UpdateTodoInput { 117 | id: ID! 118 | title: String 119 | description: String 120 | owner: String 121 | } 122 | DefaultAPIKey: 123 | Type: AWS::AppSync::ApiKey 124 | Properties: 125 | ApiId: !GetAtt TodoAPI.ApiId 126 | DependsOn: 127 | - TodoAPISchema 128 | DataSourceServiceRole: 129 | Type: AWS::IAM::Role 130 | Properties: 131 | AssumeRolePolicyDocument: 132 | Statement: 133 | - Action: sts:AssumeRole 134 | Effect: Allow 135 | Principal: 136 | Service: appsync.amazonaws.com 137 | Version: '2012-10-17' 138 | ServiceRoleDefaultPolicy: 139 | Type: AWS::IAM::Policy 140 | Properties: 141 | PolicyDocument: 142 | Statement: 143 | - Action: 144 | - dynamodb:BatchGetItem 145 | - dynamodb:BatchWriteItem 146 | - dynamodb:ConditionCheckItem 147 | - dynamodb:DeleteItem 148 | - dynamodb:DescribeTable 149 | - dynamodb:GetItem 150 | - dynamodb:GetRecords 151 | - dynamodb:GetShardIterator 152 | - dynamodb:PutItem 153 | - dynamodb:Query 154 | - dynamodb:Scan 155 | - dynamodb:UpdateItem 156 | Effect: Allow 157 | Resource: 158 | - !GetAtt TodoTable.Arn 159 | - Fn::Join: 160 | - '' 161 | - - !GetAtt TodoTable.Arn 162 | - /index/* 163 | Version: '2012-10-17' 164 | PolicyName: ServiceRoleDefaultPolicy 165 | Roles: 166 | - !Ref DataSourceServiceRole 167 | TodoTableDataSource: 168 | Type: AWS::AppSync::DataSource 169 | Properties: 170 | ApiId: !GetAtt TodoAPI.ApiId 171 | Name: table 172 | Type: AMAZON_DYNAMODB 173 | DynamoDBConfig: 174 | AwsRegion: !Ref AWS::Region 175 | TableName: !Ref TodoTable 176 | ServiceRoleArn: !GetAtt DataSourceServiceRole.Arn 177 | CreateTodoResolver: 178 | Type: AWS::AppSync::Resolver 179 | Properties: 180 | ApiId: !GetAtt TodoAPI.ApiId 181 | FieldName: createTodo 182 | TypeName: Mutation 183 | DataSourceName: table 184 | Runtime: 185 | Name: 'APPSYNC_JS' 186 | RuntimeVersion: '1.0.0' 187 | Code: | 188 | import { util } from '@aws-appsync/utils'; 189 | 190 | export function request(ctx) { 191 | const { input: values } = ctx.arguments; 192 | const key = { id: util.autoId() }; 193 | const condition = { and: [{ id: { attributeExists: false } }] }; 194 | console.log('--> create todo with requested values: ', values); 195 | return dynamodbPutRequest({ key, values, condition }); 196 | } 197 | 198 | export function response(ctx) { 199 | const { error, result } = ctx; 200 | if (error) { 201 | return util.error(error.message, error.type, result); 202 | } 203 | return ctx.result; 204 | } 205 | 206 | /** 207 | * Helper function to create a new item 208 | * @returns a PutItem request 209 | */ 210 | function dynamodbPutRequest({ key, values, condition: inCondObj }) { 211 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 212 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 213 | delete condition.expressionValues; 214 | } 215 | return { 216 | operation: 'PutItem', 217 | key: util.dynamodb.toMapValues(key), 218 | attributeValues: util.dynamodb.toMapValues(values), 219 | condition, 220 | }; 221 | } 222 | DependsOn: 223 | - TodoAPISchema 224 | - TodoTableDataSource 225 | UpdateTodoResolver: 226 | Type: AWS::AppSync::Resolver 227 | Properties: 228 | ApiId: !GetAtt TodoAPI.ApiId 229 | FieldName: updateTodo 230 | TypeName: Mutation 231 | DataSourceName: table 232 | Runtime: 233 | Name: 'APPSYNC_JS' 234 | RuntimeVersion: '1.0.0' 235 | Code: | 236 | import { util } from '@aws-appsync/utils'; 237 | 238 | export function request(ctx) { 239 | const { 240 | input: { id, ...values }, 241 | } = ctx.args; 242 | const condition = { id: { attributeExists: true } }; 243 | return dynamodbUpdateRequest({ key: { id }, values, condition }); 244 | } 245 | 246 | export function response(ctx) { 247 | const { error, result } = ctx; 248 | if (error) { 249 | return util.error(error.message, error.type, result); 250 | } 251 | return result; 252 | } 253 | 254 | function dynamodbUpdateRequest({ key, values, condition: inCondObj }) { 255 | const sets = []; 256 | const removes = []; 257 | const expressionNames = {}; 258 | const expValues = {}; 259 | 260 | for (const [k, v] of Object.entries(values)) { 261 | expressionNames[`#${k}`] = k; 262 | if (v) { 263 | sets.push(`#${k} = :${k}`); 264 | expValues[`:${k}`] = v; 265 | } else { 266 | removes.push(`#${k}`); 267 | } 268 | } 269 | 270 | console.log(`SET: ${sets.length}, REMOVE: ${removes.length}`); 271 | 272 | let expression = sets.length ? `SET ${sets.join(', ')}` : ''; 273 | expression += removes.length ? ` REMOVE ${removes.join(', ')}` : ''; 274 | 275 | console.log('update expression', expression); 276 | 277 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 278 | 279 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 280 | delete condition.expressionValues; 281 | } 282 | 283 | return { 284 | operation: 'UpdateItem', 285 | key: util.dynamodb.toMapValues(key), 286 | condition, 287 | update: { 288 | expression, 289 | expressionNames, 290 | expressionValues: util.dynamodb.toMapValues(expValues), 291 | }, 292 | }; 293 | } 294 | DependsOn: 295 | - TodoAPISchema 296 | - TodoTableDataSource 297 | DeleteTodoResolver: 298 | Type: AWS::AppSync::Resolver 299 | Properties: 300 | ApiId: !GetAtt TodoAPI.ApiId 301 | FieldName: deleteTodo 302 | TypeName: Mutation 303 | DataSourceName: table 304 | Runtime: 305 | Name: 'APPSYNC_JS' 306 | RuntimeVersion: '1.0.0' 307 | Code: | 308 | import { util } from '@aws-appsync/utils'; 309 | 310 | export function request(ctx) { 311 | return { 312 | operation: 'DeleteItem', 313 | key: util.dynamodb.toMapValues({id: ctx.args.input.id}), 314 | }; 315 | } 316 | 317 | export function response(ctx) { 318 | const { error, result } = ctx; 319 | if (error) { 320 | return util.error(error.message, error.type, result); 321 | } 322 | return ctx.result; 323 | } 324 | DependsOn: 325 | - TodoAPISchema 326 | - TodoTableDataSource 327 | GetTodoResolver: 328 | Type: AWS::AppSync::Resolver 329 | Properties: 330 | ApiId: !GetAtt TodoAPI.ApiId 331 | FieldName: getTodo 332 | TypeName: Query 333 | DataSourceName: table 334 | Runtime: 335 | Name: 'APPSYNC_JS' 336 | RuntimeVersion: '1.0.0' 337 | Code: | 338 | import { util } from '@aws-appsync/utils'; 339 | 340 | export function request(ctx) { 341 | return { 342 | operation: 'GetItem', 343 | key: util.dynamodb.toMapValues({ id: ctx.args.id}), 344 | }; 345 | } 346 | 347 | export function response(ctx) { 348 | const { error, result } = ctx; 349 | if (error) { 350 | return util.error(error.message, error.type, result); 351 | } 352 | return ctx.result; 353 | } 354 | DependsOn: 355 | - TodoAPISchema 356 | - TodoTableDataSource 357 | ListTodosResolver: 358 | Type: AWS::AppSync::Resolver 359 | Properties: 360 | ApiId: !GetAtt TodoAPI.ApiId 361 | FieldName: listTodos 362 | TypeName: Query 363 | DataSourceName: table 364 | Runtime: 365 | Name: 'APPSYNC_JS' 366 | RuntimeVersion: '1.0.0' 367 | Code: | 368 | import { util } from '@aws-appsync/utils'; 369 | 370 | export function request(ctx) { 371 | const { filter: f, limit = 20, nextToken } = ctx.args; 372 | const filter = f ? JSON.parse(util.transform.toDynamoDBFilterExpression(f)) : null; 373 | return { operation: 'Scan', filter, limit, nextToken }; 374 | } 375 | 376 | export function response(ctx) { 377 | const { error, result } = ctx; 378 | if (error) { 379 | return util.error(error.message, error.type, result); 380 | } 381 | const { items = [], nextToken } = result; 382 | return { items, nextToken }; 383 | } 384 | DependsOn: 385 | - TodoAPISchema 386 | - TodoTableDataSource 387 | QueryTodosResolver: 388 | Type: AWS::AppSync::Resolver 389 | Properties: 390 | ApiId: !GetAtt TodoAPI.ApiId 391 | FieldName: queryTodosByOwnerIndex 392 | TypeName: Query 393 | DataSourceName: table 394 | Runtime: 395 | Name: 'APPSYNC_JS' 396 | RuntimeVersion: '1.0.0' 397 | Code: | 398 | import { util } from '@aws-appsync/utils'; 399 | 400 | export function request(ctx) { 401 | const { owner, first = 20, after } = ctx.args; 402 | return dynamodbQueryRequest('owner', owner, 'owner-index', first, after); 403 | } 404 | 405 | export function response(ctx) { 406 | const { error, result } = ctx; 407 | if (error) { 408 | return util.error(error.message, error.type, result); 409 | } 410 | return result; 411 | } 412 | 413 | function dynamodbQueryRequest(key, value, index, limit, nextToken) { 414 | const expression = `#key = :key`; 415 | const expressionNames = { '#key': key }; 416 | const expressionValues = util.dynamodb.toMapValues({ ':key': value }); 417 | return { 418 | operation: 'Query', 419 | query: { expression, expressionNames, expressionValues }, 420 | index, 421 | limit, 422 | nextToken, 423 | scanIndexForward: true, 424 | select: 'ALL_ATTRIBUTES', 425 | }; 426 | } 427 | DependsOn: 428 | - TodoAPISchema 429 | - TodoTableDataSource 430 | TodoTable: 431 | Type: AWS::DynamoDB::Table 432 | Properties: 433 | KeySchema: 434 | - AttributeName: id 435 | KeyType: HASH 436 | AttributeDefinitions: 437 | - AttributeName: id 438 | AttributeType: S 439 | - AttributeName: owner 440 | AttributeType: S 441 | GlobalSecondaryIndexes: 442 | - IndexName: owner-index 443 | KeySchema: 444 | - AttributeName: owner 445 | KeyType: HASH 446 | Projection: 447 | ProjectionType: ALL 448 | ProvisionedThroughput: 449 | ReadCapacityUnits: 5 450 | WriteCapacityUnits: 5 451 | ProvisionedThroughput: 452 | ReadCapacityUnits: 5 453 | WriteCapacityUnits: 5 454 | UpdateReplacePolicy: Retain 455 | DeletionPolicy: Retain 456 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/README.md: -------------------------------------------------------------------------------- 1 | # Todo API 2 | 3 | This folder provides the implementation for a Todo API implemented with AppSync JavaScript resolvers. 4 | The API allows you to interact with a Todo type: 5 | 6 | ```graphql 7 | type Todo { 8 | id: ID! 9 | title: String 10 | description: String 11 | owner: String 12 | } 13 | ``` 14 | 15 | The [functions](./functions/) folder contains the code for the AppSync functions, while the [resolvers](./resolvers/) folder contains the code for the pipeline resolvers. The resolvers in this API do not implement any before or after business logic, and the same code is use for all resolvers. 16 | 17 | ## Deploy the stack 18 | 19 | Deploy this stack by using [template.yaml](./template.yaml) from the Cloudformation console or using the AWS CLI. 20 | 21 | With the AWS CLI, from this folder: 22 | 23 | ```sh 24 | aws cloudformation deploy --template-file ./template.yaml --stack-name demo-todo-api-js --capabilities CAPABILITY_IAM 25 | ``` 26 | 27 | Once deployed, you can find your API **TodoAPIwithJs** in the AWS console. 28 | 29 | ## Delete the stack 30 | 31 | To delete your resources: visit the Cloudformation console and delete the stack. 32 | 33 | With the AWS CLI: 34 | 35 | ```sh 36 | aws cloudformation delete-stack --stack-name demo-todo-api-js 37 | ``` 38 | 39 | Note: The DynamoDB table deployed by this template is retained when the stack is deleted. 40 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/createItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: creates a new item in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | /** 9 | * Creates a new item in a DynamoDB table 10 | * @param ctx contextual information about the request 11 | */ 12 | export function request(ctx) { 13 | const { input: values } = ctx.arguments; 14 | const key = { id: util.autoId() }; 15 | const condition = { id: { attributeExists: false } }; 16 | console.log('--> create todo with requested values: ', values); 17 | return dynamodbPutRequest({ key, values, condition }); 18 | } 19 | 20 | /** 21 | * Returns the result 22 | * @param ctx contextual information about the request 23 | */ 24 | export function response(ctx) { 25 | const { error, result } = ctx; 26 | if (error) { 27 | return util.appendError(error.message, error.type, result); 28 | } 29 | return ctx.result; 30 | } 31 | 32 | /** 33 | * Helper function to create a new item 34 | * @returns a PutItem request 35 | */ 36 | function dynamodbPutRequest({ key, values, condition: inCondObj }) { 37 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 38 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 39 | delete condition.expressionValues; 40 | } 41 | return { 42 | operation: 'PutItem', 43 | key: util.dynamodb.toMapValues(key), 44 | attributeValues: util.dynamodb.toMapValues(values), 45 | condition, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/deleteItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: deletes an item in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | export function request(ctx) { 9 | const { id } = ctx.args.input; 10 | return dynamodbDeleteRequest({ id }); 11 | } 12 | 13 | export function response(ctx) { 14 | const { error, result } = ctx; 15 | if (error) { 16 | return util.appendError(error.message, error.type, result); 17 | } 18 | return ctx.result; 19 | } 20 | 21 | function dynamodbDeleteRequest(key) { 22 | return { 23 | operation: 'DeleteItem', 24 | key: util.dynamodb.toMapValues(key), 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/getItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: Fetches an item in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | /** 9 | * Request a single item from the attached DynamoDB table datasource 10 | * @param ctx the request context 11 | */ 12 | export function request(ctx) { 13 | const { id } = ctx.args; 14 | return dynamoDBGetItemRequest({ id }); 15 | } 16 | 17 | /** 18 | * Returns the result 19 | * @param ctx the request context 20 | */ 21 | export function response(ctx) { 22 | const { error, result } = ctx; 23 | if (error) { 24 | return util.appendError(error.message, error.type, result); 25 | } 26 | return ctx.result; 27 | } 28 | 29 | /** 30 | * A helper function to get a DynamoDB it 31 | * @param key a set of keys for the item 32 | * @returns a DynamoDB Get request 33 | */ 34 | function dynamoDBGetItemRequest(key) { 35 | return { 36 | operation: 'GetItem', 37 | key: util.dynamodb.toMapValues(key), 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/listItems.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: lists items in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | export function request(ctx) { 9 | const { filter, limit = 20, nextToken } = ctx.args; 10 | return dynamoDBScanRequest({ filter, limit, nextToken }); 11 | } 12 | 13 | export function response(ctx) { 14 | const { error, result } = ctx; 15 | if (error) { 16 | return util.appendError(error.message, error.type, result); 17 | } 18 | const { items = [], nextToken } = result; 19 | return { items, nextToken }; 20 | } 21 | 22 | function dynamoDBScanRequest({ filter: f, limit, nextToken }) { 23 | const filter = f ? JSON.parse(util.transform.toDynamoDBFilterExpression(f)) : null; 24 | 25 | return { operation: 'Scan', filter, limit, nextToken }; 26 | } 27 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/out.js: -------------------------------------------------------------------------------- 1 | // apis/todo-api/functions/scratch.js 2 | import { util as util3 } from "@aws-appsync/utils"; 3 | 4 | // utils/dynamodb/index.js 5 | import { util } from "@aws-appsync/utils"; 6 | 7 | // utils/http/index.js 8 | import { util as util2 } from "@aws-appsync/utils"; 9 | function publishToSNSRequest(topicArn, values) { 10 | const arn = util2.urlEncode(topicArn); 11 | const message = util2.urlEncode(JSON.stringify(values)); 12 | let body = `Action=Publish&Version=2010-03-31&TopicArn=${arn}`; 13 | body += `$&Message=${message}`; 14 | return { 15 | method: "POST", 16 | resourcePath: "/", 17 | params: { 18 | body, 19 | headers: { 20 | "content-type": "application/x-www-form-urlencoded" 21 | } 22 | } 23 | }; 24 | } 25 | 26 | // apis/todo-api/functions/scratch.js 27 | function request(ctx) { 28 | const { input: values } = ctx.arguments; 29 | return publishToSNSRequest("TOPIC_ARN", values); 30 | } 31 | function response(ctx) { 32 | const { error, result } = ctx; 33 | if (error) { 34 | return util3.appendError(error.message, error.type, result); 35 | } 36 | return ctx.result; 37 | } 38 | export { 39 | request, 40 | response 41 | }; 42 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/queryItems.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: queries items on a given index in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | export function request(ctx) { 9 | const { owner, first = 20, after } = ctx.args; 10 | return dynamodbQueryRequest('owner', owner, 'owner-index', first, after); 11 | } 12 | 13 | export function response(ctx) { 14 | const { error, result } = ctx; 15 | if (error) { 16 | return util.appendError(error.message, error.type, result); 17 | } 18 | return result; 19 | } 20 | 21 | function dynamodbQueryRequest(key, value, index, limit, nextToken) { 22 | const expression = `#key = :key`; 23 | const expressionNames = { '#key': key }; 24 | const expressionValues = util.dynamodb.toMapValues({ ':key': value }); 25 | return { 26 | operation: 'Query', 27 | query: { expression, expressionNames, expressionValues }, 28 | index, 29 | limit, 30 | nextToken, 31 | scanIndexForward: true, 32 | select: 'ALL_ATTRIBUTES', 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/scratch.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as dynamodb from '../../../utils/dynamodb'; 3 | import * as http from '../../../utils/http'; 4 | 5 | export function request(ctx) { 6 | const { input: values } = ctx.arguments; 7 | // const key = { id: util.autoId() }; 8 | // const condition = { id: { attributeExists: false } }; 9 | // return dynamodb.update({ key, values, condition }); 10 | return http.publishToSNSRequest('TOPIC_ARN', values); 11 | } 12 | 13 | export function response(ctx) { 14 | const { error, result } = ctx; 15 | if (error) { 16 | return util.appendError(error.message, error.type, result); 17 | } 18 | return ctx.result; 19 | } 20 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/functions/udpateItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync function: updates a new item in a DynamoDB table. 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | 6 | import { util } from '@aws-appsync/utils'; 7 | 8 | export function request(ctx) { 9 | const { 10 | input: { id, ...values }, 11 | } = ctx.args; 12 | const condition = { id: { attributeExists: true } }; 13 | return dynamodbUpdateRequest({ key: { id }, values, condition }); 14 | } 15 | 16 | export function response(ctx) { 17 | const { error, result } = ctx; 18 | if (error) { 19 | return util.appendError(error.message, error.type, result); 20 | } 21 | return result; 22 | } 23 | 24 | function dynamodbUpdateRequest({ key, values, condition: inCondObj }) { 25 | const sets = []; 26 | const removes = []; 27 | const expressionNames = {}; 28 | const expValues = {}; 29 | 30 | for (const [k, v] of Object.entries(values)) { 31 | expressionNames[`#${k}`] = k; 32 | if (v) { 33 | sets.push(`#${k} = :${k}`); 34 | expValues[`:${k}`] = v; 35 | } else { 36 | removes.push(`#${k}`); 37 | } 38 | } 39 | 40 | console.log(`SET: ${sets.length}, REMOVE: ${removes.length}`); 41 | 42 | let expression = sets.length ? `SET ${sets.join(', ')}` : ''; 43 | expression += removes.length ? ` REMOVE ${removes.join(', ')}` : ''; 44 | 45 | console.log('update expression', expression); 46 | 47 | const condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj)); 48 | 49 | if (condition.expressionValues && !Object.keys(condition.expressionValues).length) { 50 | delete condition.expressionValues; 51 | } 52 | 53 | return { 54 | operation: 'UpdateItem', 55 | key: util.dynamodb.toMapValues(key), 56 | condition, 57 | update: { 58 | expression, 59 | expressionNames, 60 | expressionValues: util.dynamodb.toMapValues(expValues), 61 | }, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/resolvers/default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AppSync resolver: implements before and after business logic for your pipeline 3 | * Find more samples and templates at https://github.com/aws-samples/aws-appsync-resolver-samples 4 | */ 5 | /** 6 | * Called before the request function of the first AppSync function in the pipeline. 7 | * @param ctx The context object that holds contextual information about the function invocation. 8 | */ 9 | export function request(ctx) { 10 | return {}; 11 | } 12 | /** 13 | * Called after the response function of the last AppSync function in the pipeline. 14 | * @param ctx The context object that holds contextual information about the function invocation. 15 | */ 16 | export function response(ctx) { 17 | return ctx.prev.result; 18 | } 19 | -------------------------------------------------------------------------------- /examples/cloudformation/todo-api-pipeline-cfn/schema.graphql: -------------------------------------------------------------------------------- 1 | input CreateTodoInput { 2 | title: String 3 | description: String 4 | owner: String 5 | } 6 | 7 | input DeleteTodoInput { 8 | id: ID! 9 | } 10 | 11 | type Mutation { 12 | createTodo(input: CreateTodoInput!): Todo 13 | updateTodo(input: UpdateTodoInput!): Todo 14 | deleteTodo(input: DeleteTodoInput!): Todo 15 | } 16 | 17 | type Query { 18 | getTodo(id: ID!): Todo 19 | listTodos( 20 | filter: TableTodoFilterInput 21 | limit: Int 22 | nextToken: String 23 | ): TodoConnection 24 | queryTodosByOwnerIndex( 25 | owner: String! 26 | first: Int 27 | after: String 28 | ): TodoConnection 29 | } 30 | 31 | type Subscription { 32 | onCreateTodo(id: ID, title: String, description: String, owner: String): Todo 33 | @aws_subscribe(mutations: ["createTodo"]) 34 | onUpdateTodo(id: ID, title: String, description: String, owner: String): Todo 35 | @aws_subscribe(mutations: ["updateTodo"]) 36 | onDeleteTodo(id: ID, title: String, description: String, owner: String): Todo 37 | @aws_subscribe(mutations: ["deleteTodo"]) 38 | } 39 | 40 | input TableBooleanFilterInput { 41 | ne: Boolean 42 | eq: Boolean 43 | } 44 | 45 | input TableFloatFilterInput { 46 | ne: Float 47 | eq: Float 48 | le: Float 49 | lt: Float 50 | ge: Float 51 | gt: Float 52 | contains: Float 53 | notContains: Float 54 | between: [Float] 55 | } 56 | 57 | input TableIDFilterInput { 58 | ne: ID 59 | eq: ID 60 | le: ID 61 | lt: ID 62 | ge: ID 63 | gt: ID 64 | contains: ID 65 | notContains: ID 66 | between: [ID] 67 | beginsWith: ID 68 | } 69 | 70 | input TableIntFilterInput { 71 | ne: Int 72 | eq: Int 73 | le: Int 74 | lt: Int 75 | ge: Int 76 | gt: Int 77 | contains: Int 78 | notContains: Int 79 | between: [Int] 80 | } 81 | 82 | input TableStringFilterInput { 83 | ne: String 84 | eq: String 85 | le: String 86 | lt: String 87 | ge: String 88 | gt: String 89 | contains: String 90 | notContains: String 91 | between: [String] 92 | beginsWith: String 93 | } 94 | 95 | input TableTodoFilterInput { 96 | id: TableIDFilterInput 97 | title: TableStringFilterInput 98 | description: TableStringFilterInput 99 | owner: TableStringFilterInput 100 | } 101 | 102 | type Todo { 103 | id: ID! 104 | title: String 105 | description: String 106 | owner: String 107 | } 108 | 109 | type TodoConnection { 110 | items: [Todo] 111 | nextToken: String 112 | } 113 | 114 | input UpdateTodoInput { 115 | id: ID! 116 | title: String 117 | description: String 118 | owner: String 119 | } 120 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/README.md: -------------------------------------------------------------------------------- 1 | # Serverless Framework AWS AppSync Example 2 | 3 | Type `Post` is using Lambda data source 4 | 5 | ```graphql 6 | type Post { 7 | id: ID! 8 | title: String 9 | body: String 10 | } 11 | ``` 12 | 13 | Type `User` is using HTTP data source 14 | 15 | ```graphql 16 | type User { 17 | id: ID! 18 | firstName: String 19 | lastName: String 20 | } 21 | ``` 22 | 23 | Type `Todo` is using DynamoDB data source with util examples in the resolvers 24 | 25 | ```graphql 26 | type Todo { 27 | id: ID! 28 | title: String 29 | completed: Boolean! 30 | } 31 | ``` 32 | 33 | ## Deploy the stack 34 | 35 | Deploy this stack by using Serverless Framework as defined in [serverless.yml](./serverless.yml) by running `serverless deploy`. 36 | 37 | ## Delete the resource 38 | 39 | To delete your resources: visit the Cloudformation console and delete the stack. 40 | 41 | With the AWS CLI: 42 | 43 | ```sh 44 | aws cloudformation delete-stack --stack-name sls-appsync-example-dev 45 | ``` 46 | 47 | Note: The stack name `sls-appsync-example-dev` above assumes the `service` and `environemnt` attributes in `serverless.yml` were not modified. 48 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/lambda/index.js: -------------------------------------------------------------------------------- 1 | module.exports.handler = async (event) => { 2 | const posts = [ 3 | { 4 | id: '1', 5 | title: 'Post One', 6 | }, 7 | { 8 | id: '2', 9 | title: 'Post Two', 10 | }, 11 | { 12 | id: '3', 13 | title: 'Post Three', 14 | }, 15 | { 16 | id: '4', 17 | title: 'Post Four', 18 | }, 19 | { 20 | id: '5', 21 | title: 'Post Five', 22 | }, 23 | ]; 24 | 25 | if (event.field === 'getPost') { 26 | return posts.find((post) => post.id === event.arguments.id); 27 | } else { 28 | return posts; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "serverless-appsync-plugin": "^2.7.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/ddb/addTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | return ddb.put({ 6 | key: { 7 | id: util.autoId(), 8 | }, 9 | item: ctx.arguments.input, 10 | }); 11 | } 12 | 13 | export function response(ctx) { 14 | return ctx.result; 15 | } 16 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/ddb/getTodo.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export function request(ctx) { 4 | return ddb.get({ key: { id: ctx.arguments.id } }); 5 | } 6 | 7 | export function response(ctx) { 8 | return ctx.result; 9 | } 10 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/ddb/listTodos.js: -------------------------------------------------------------------------------- 1 | export function request(ctx) { 2 | return { 3 | operation: 'Scan', 4 | }; 5 | } 6 | 7 | export function response(ctx) { 8 | return ctx.result.items; 9 | } 10 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/http/getUser.js: -------------------------------------------------------------------------------- 1 | export function request(ctx) { 2 | return { 3 | method: 'GET', 4 | resourcePath: '/users/' + ctx.args.id, 5 | params: { 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | }, 10 | }; 11 | } 12 | 13 | export function response(ctx) { 14 | return JSON.parse(ctx.result.body); 15 | } 16 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/http/listUsers.js: -------------------------------------------------------------------------------- 1 | export function request() { 2 | return { 3 | method: 'GET', 4 | resourcePath: '/users', 5 | }; 6 | } 7 | 8 | export function response(ctx) { 9 | const data = JSON.parse(ctx.result.body); 10 | return data.users; 11 | } 12 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/lambda/getPost.js: -------------------------------------------------------------------------------- 1 | export function request(ctx) { 2 | return { 3 | operation: 'Invoke', 4 | payload: { field: ctx.info.fieldName, arguments: ctx.args }, 5 | }; 6 | } 7 | 8 | export function response(ctx) { 9 | return ctx.result; 10 | } 11 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/resolvers/lambda/listPosts.js: -------------------------------------------------------------------------------- 1 | export function request(ctx) { 2 | return { 3 | operation: 'Invoke', 4 | payload: { field: ctx.info.fieldName }, 5 | }; 6 | } 7 | 8 | export function response(ctx) { 9 | return ctx.result; 10 | } 11 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | type Query { 7 | listPosts: [Post] 8 | listUsers: [User] 9 | listTodos: [Todo] 10 | getPost(id: ID!): Post 11 | getUser(id: ID!): User 12 | getTodo(id: ID!): Todo 13 | } 14 | 15 | type Mutation { 16 | addTodo(input: AddTodoInput): Todo 17 | } 18 | 19 | type Post { 20 | id: ID! 21 | title: String 22 | body: String 23 | } 24 | 25 | type User { 26 | id: ID! 27 | firstName: String 28 | lastName: String 29 | } 30 | 31 | type Todo { 32 | id: ID! 33 | title: String 34 | completed: Boolean! 35 | } 36 | 37 | input AddTodoInput { 38 | title: String 39 | completed: Boolean 40 | } 41 | -------------------------------------------------------------------------------- /examples/serverless/lambda-http-ddb-datasources/serverless.yml: -------------------------------------------------------------------------------- 1 | service: sls-appsync-example 2 | frameworkVersion: '3' 3 | 4 | provider: 5 | name: aws 6 | 7 | resources: 8 | Resources: 9 | myTodosTable: 10 | Type: AWS::DynamoDB::Table 11 | Properties: 12 | TableName: myTodosTable 13 | AttributeDefinitions: 14 | - AttributeName: id 15 | AttributeType: S 16 | KeySchema: 17 | - AttributeName: id 18 | KeyType: HASH 19 | ProvisionedThroughput: 20 | ReadCapacityUnits: 1 21 | WriteCapacityUnits: 1 22 | plugins: 23 | - serverless-appsync-plugin 24 | 25 | appSync: 26 | name: sls-appsync 27 | 28 | logging: 29 | level: ALL 30 | retentionInDays: 14 31 | 32 | authentication: 33 | type: API_KEY 34 | 35 | apiKeys: 36 | - name: myKey 37 | expiresAfter: 7d 38 | 39 | dataSources: 40 | myTodosTable: 41 | type: AMAZON_DYNAMODB 42 | description: 'My appsync table' 43 | config: 44 | tableName: myTodosTable 45 | myLambda: 46 | type: 'AWS_LAMBDA' 47 | config: 48 | function: 49 | handler: 'lambda/index.handler' 50 | runtime: nodejs18.x 51 | package: 52 | individually: true 53 | patterns: 54 | - '!./node_modules**' # exclude root files 55 | - '!./**' 56 | - './lambda/**' 57 | myEndpoint: 58 | type: 'HTTP' 59 | config: 60 | endpoint: https://dummyjson.com 61 | 62 | resolvers: 63 | Query.listPosts: 64 | dataSource: myLambda 65 | kind: UNIT 66 | code: 'resolvers/lambda/listPosts.js' 67 | Query.listUsers: 68 | dataSource: myEndpoint 69 | kind: UNIT 70 | code: 'resolvers/http/listUsers.js' 71 | Query.listTodos: 72 | dataSource: myTodosTable 73 | kind: UNIT 74 | code: 'resolvers/ddb/listTodos.js' 75 | 76 | Query.getPost: 77 | dataSource: myLambda 78 | kind: UNIT 79 | code: 'resolvers/lambda/getPost.js' 80 | Query.getUser: 81 | dataSource: myEndpoint 82 | kind: UNIT 83 | code: 'resolvers/http/getUser.js' 84 | Query.getTodo: 85 | dataSource: myTodosTable 86 | kind: UNIT 87 | code: 'resolvers/ddb/getTodo.js' 88 | 89 | Mutation.addTodo: 90 | dataSource: myTodosTable 91 | kind: UNIT 92 | code: 'resolvers/ddb/addTodo.js' 93 | -------------------------------------------------------------------------------- /examples/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.82.2" 6 | constraints = "~> 5.82.2" 7 | hashes = [ 8 | "h1:ce6Dw2y4PpuqAPtnQ0dO270dRTmwEARqnfffrE1VYJ8=", 9 | "zh:0262fc96012fb7e173e1b7beadd46dfc25b1dc7eaef95b90e936fc454724f1c8", 10 | "zh:397413613d27f4f54d16efcbf4f0a43c059bd8d827fe34287522ae182a992f9b", 11 | "zh:436c0c5d56e1da4f0a4c13129e12a0b519d12ab116aed52029b183f9806866f3", 12 | "zh:4d942d173a2553d8d532a333a0482a090f4e82a2238acf135578f163b6e68470", 13 | "zh:624aebc549bfbce06cc2ecfd8631932eb874ac7c10eb8466ce5b9a2fbdfdc724", 14 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 15 | "zh:9e632dee2dfdf01b371cca7854b1ec63ceefa75790e619b0642b34d5514c6733", 16 | "zh:a07567acb115b60a3df8f6048d12735b9b3bcf85ec92a62f77852e13d5a3c096", 17 | "zh:ab7002df1a1be6432ac0eb1b9f6f0dd3db90973cd5b1b0b33d2dae54553dfbd7", 18 | "zh:bc1ff65e2016b018b3e84db7249b2cd0433cb5c81dc81f9f6158f2197d6b9fde", 19 | "zh:bcad84b1d767f87af6e1ba3dc97fdb8f2ad5de9224f192f1412b09aba798c0a8", 20 | "zh:cf917dceaa0f9d55d9ff181b5dcc4d1e10af21b6671811b315ae2a6eda866a2a", 21 | "zh:d8e90ecfb3216f3cc13ccde5a16da64307abb6e22453aed2ac3067bbf689313b", 22 | "zh:d9054e0e40705df729682ad34c20db8695d57f182c65963abd151c6aba1ab0d3", 23 | "zh:ecf3a4f3c57eb7e89f71b8559e2a71e4cdf94eea0118ec4f2cb37e4f4d71a069", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/terraform/main.tf: -------------------------------------------------------------------------------- 1 | // Create AppSync API 2 | resource "aws_appsync_graphql_api" "appsync_api" { 3 | authentication_type = "API_KEY" 4 | name = "AppSync Terraform API" 5 | 6 | schema = file("schema.graphql") 7 | } 8 | 9 | // Create API Key 10 | resource "aws_appsync_api_key" "appsync_api_key" { 11 | api_id = aws_appsync_graphql_api.appsync_api.id 12 | 13 | // Set the expiration 7 days from now - https://developer.hashicorp.com/terraform/language/functions/timeadd 14 | expires = timeadd(timestamp(), "168h") 15 | } 16 | 17 | // Add HTTP Datasource 18 | resource "aws_appsync_datasource" "todo_http_datasource" { 19 | api_id = aws_appsync_graphql_api.appsync_api.id 20 | name = "lambdaPosts" 21 | type = "HTTP" 22 | 23 | http_config { 24 | endpoint = "" // Replace this with actual endpoint. For example, https://jsonplaceholder.typicode.com 25 | } 26 | } 27 | 28 | // Add resolver for Query.listTodos 29 | resource "aws_appsync_resolver" "listTodos" { 30 | api_id = aws_appsync_graphql_api.appsync_api.id 31 | type = "Query" 32 | field = "listTodos" 33 | 34 | data_source = aws_appsync_datasource.todo_http_datasource.name 35 | kind = "UNIT" 36 | 37 | code = file("resolvers/listTodos.js") 38 | 39 | runtime { 40 | name = "APPSYNC_JS" 41 | runtime_version = "1.0.0" 42 | } 43 | } 44 | 45 | // Add resolver for Query.getTodo 46 | resource "aws_appsync_resolver" "getTodo" { 47 | api_id = aws_appsync_graphql_api.appsync_api.id 48 | type = "Query" 49 | field = "getTodo" 50 | 51 | data_source = aws_appsync_datasource.todo_http_datasource.name 52 | kind = "UNIT" 53 | 54 | code = file("resolvers/getTodo.js") 55 | 56 | runtime { 57 | name = "APPSYNC_JS" 58 | runtime_version = "1.0.0" 59 | } 60 | caching_config { 61 | ttl = 3600 62 | } 63 | } 64 | 65 | 66 | // To Enable caching uncomment the following code block 67 | # resource "aws_appsync_api_cache" "cache_config" { 68 | # api_id = aws_appsync_graphql_api.appsync_api.id 69 | # api_caching_behavior = "PER_RESOLVER_CACHING" 70 | # type = "LARGE" 71 | # ttl = 3600 72 | # } 73 | -------------------------------------------------------------------------------- /examples/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.82.2" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /examples/terraform/resolvers/getTodo.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | return fetch(`/todos/${ctx.args.id}`); 5 | } 6 | 7 | export function response(ctx) { 8 | const { statusCode, body } = ctx.result; 9 | // if response is 200, return the response 10 | if (statusCode === 200) { 11 | return JSON.parse(body); // this will depend on the response shape of the actual api/datasource 12 | } 13 | // if response is not 200, append the response to error block. 14 | util.appendError(body, statusCode); 15 | } 16 | 17 | function fetch(resourcePath, options) { 18 | const { method = 'GET', headers, body, query } = options; 19 | 20 | return { 21 | resourcePath, 22 | method, 23 | params: { headers, query, body }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /examples/terraform/resolvers/listTodos.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | return fetch(`/todos`); 5 | } 6 | 7 | export function response(ctx) { 8 | const { statusCode, body } = ctx.result; 9 | // if response is 200, return the response 10 | if (statusCode === 200) { 11 | return JSON.parse(body); // this will depend on the response shape of the actual api/datasource 12 | } 13 | // if response is not 200, append the response to error block. 14 | util.appendError(body, statusCode); 15 | } 16 | 17 | function fetch(resourcePath, options) { 18 | const { method = 'GET', headers, body, query } = options; 19 | 20 | return { 21 | resourcePath, 22 | method, 23 | params: { headers, query, body }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /examples/terraform/schema.graphql: -------------------------------------------------------------------------------- 1 | type Todo { 2 | userId: Int 3 | id: ID 4 | title: String 5 | completed: Boolean 6 | } 7 | type Query { 8 | getTodo(id: ID!): Todo 9 | listTodos: [Todo] 10 | } 11 | 12 | schema { 13 | query: Query 14 | } 15 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-resolver-samples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "aws-appsync-resolver-samples", 9 | "version": "1.0.0", 10 | "license": "MIT-0" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-appsync-resolver-samples", 3 | "version": "1.0.0", 4 | "description": "Getting started samples for AppSync JavaScript resolvers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/aws-samples/aws-appsync-resolver-samples.git" 12 | }, 13 | "keywords": [ 14 | "appsync", 15 | "graphql", 16 | "javascript" 17 | ], 18 | "license": "MIT-0", 19 | "bugs": { 20 | "url": "https://github.com/aws-samples/aws-appsync-resolver-samples/issues" 21 | }, 22 | "homepage": "https://github.com/aws-samples/aws-appsync-resolver-samples#readme" 23 | } 24 | -------------------------------------------------------------------------------- /samples/NONE/enhancedSubscription.js: -------------------------------------------------------------------------------- 1 | // TITLE: Set up an enhanced subscription 2 | 3 | import { util, extensions } from '@aws-appsync/utils'; 4 | 5 | /** 6 | * Sends an empty payload as the subscription is established 7 | * @param {*} ctx the context 8 | * @returns {import('@aws-appsync/utils').NONERequest} the request 9 | */ 10 | export function request(ctx) { 11 | //noop 12 | return { payload: {} }; 13 | } 14 | 15 | /** 16 | * Creates an enhanced subscription 17 | * @param {import('@aws-appsync/utils').Context} ctx the context 18 | * @returns {*} the result 19 | */ 20 | export function response(ctx) { 21 | // the logged in user group 22 | const groups = ['admin', 'operators']; 23 | // or use the user's own groups 24 | // const groups = ctx.identity.groups 25 | 26 | // This filter sets up a subscription that is triggered when: 27 | // - a mutation with severity >= 7 and priority high or medium is made 28 | // - or a mtuation with classification "security" is made and the user belongs to the "admin" or "operators" group 29 | const filter = util.transform.toSubscriptionFilter({ 30 | or: [ 31 | { and: [{ severity: { ge: 7 } }, { priority: { in: ['high', 'medium'] } }] }, 32 | { and: [{ classification: { eq: 'security' } }, { group: { in: groups } }] }, 33 | ], 34 | }); 35 | console.log(filter); 36 | extensions.setSubscriptionFilter(filter); 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /samples/NONE/localPublish.js: -------------------------------------------------------------------------------- 1 | // TITLE: Locally publish a message 2 | 3 | import { util } from '@aws-appsync/utils'; 4 | 5 | /** 6 | * Publishes an event localy 7 | * @param {import('@aws-appsync/utils').Context} ctx the context 8 | * @returns {import('@aws-appsync/utils').NONERequest} the request 9 | */ 10 | export function request(ctx) { 11 | return { 12 | payload: { 13 | body: ctx.args.body, 14 | to: ctx.args.to, 15 | from: /** @type {import('@aws-appsync/utils').AppSyncIdentityCognito} */ (ctx.identity) 16 | .username, 17 | sentAt: util.time.nowISO8601(), 18 | }, 19 | }; 20 | } 21 | 22 | /** 23 | * Forward the payload in the `result` object 24 | * @param {import('@aws-appsync/utils').Context} ctx the context 25 | * @returns {*} the result 26 | */ 27 | export function response(ctx) { 28 | return ctx.result; 29 | } 30 | -------------------------------------------------------------------------------- /samples/dynamodb/batch/batchDeleteItems.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Deletes items from DynamoDB tables in batches with the provided `id` keys 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns {import('@aws-appsync/utils').DynamoDBBatchDeleteItemRequest} the request 7 | */ 8 | export function request(ctx) { 9 | return { 10 | operation: 'BatchDeleteItem', 11 | tables: { 12 | Posts: { 13 | keys: ctx.args.ids.map((id) => util.dynamodb.toMapValues({ id })), 14 | }, 15 | }, 16 | }; 17 | } 18 | 19 | /** 20 | * Returns the BatchDeleteItem table results 21 | * @param {import('@aws-appsync/utils').Context} ctx the context 22 | * @returns {[*]} the items 23 | */ 24 | export function response(ctx) { 25 | if (ctx.error) { 26 | util.error(ctx.error.message, ctx.error.type); 27 | } 28 | return ctx.result.data.Posts; 29 | } 30 | -------------------------------------------------------------------------------- /samples/dynamodb/batch/batchGetItems.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Gets items from the DynamoDB tables in batches with the provided `id` keys 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns {import('@aws-appsync/utils').DynamoDBBatchGetItemRequest} the request 7 | */ 8 | export function request(ctx) { 9 | return { 10 | operation: 'BatchGetItem', 11 | tables: { 12 | Posts: { 13 | keys: ctx.args.ids.map((id) => util.dynamodb.toMapValues({ id })), 14 | consistentRead: true, 15 | }, 16 | }, 17 | }; 18 | } 19 | 20 | /** 21 | * Returns the BatchGetItem table items 22 | * @param {import('@aws-appsync/utils').Context} ctx the context 23 | * @returns {[*]} the items 24 | */ 25 | export function response(ctx) { 26 | if (ctx.error) { 27 | util.error(ctx.error.message, ctx.error.type); 28 | } 29 | return ctx.result.data.Posts; 30 | } 31 | -------------------------------------------------------------------------------- /samples/dynamodb/batch/batchPutItems.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Gets items from the DynamoDB tables in batches with the provided `id` keys 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns {import('@aws-appsync/utils').DynamoDBBatchPutItemRequest} the request 7 | */ 8 | export function request(ctx) { 9 | return { 10 | operation: 'BatchPutItem', 11 | tables: { 12 | Posts: ctx.args.posts.map((post) => util.dynamodb.toMapValues(post)), 13 | }, 14 | }; 15 | } 16 | 17 | /** 18 | * Returns the BatchPutItem table results 19 | * @param {import('@aws-appsync/utils').Context} ctx the context 20 | * @returns {[*]} the items 21 | */ 22 | export function response(ctx) { 23 | if (ctx.error) { 24 | util.error(ctx.error.message, ctx.error.type); 25 | } 26 | return ctx.result.data.Posts; 27 | } 28 | -------------------------------------------------------------------------------- /samples/dynamodb/general/deleteItem.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export const request = (ctx) => ddb.remove({ key: { id: ctx.args.id } }); 4 | export const response = (ctx) => ctx.result; 5 | -------------------------------------------------------------------------------- /samples/dynamodb/general/getItem.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export const request = (ctx) => ddb.get({ key: { id: ctx.args.id } }); 4 | export const response = (ctx) => ctx.result; 5 | -------------------------------------------------------------------------------- /samples/dynamodb/general/listItems.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export function request(ctx) { 4 | const { limit = 10, nextToken } = ctx.args; 5 | return ddb.scan({ limit, nextToken }); 6 | } 7 | 8 | export function response(ctx) { 9 | const { items, nextToken } = ctx.result; 10 | return { items: items ?? [], nextToken }; 11 | } 12 | -------------------------------------------------------------------------------- /samples/dynamodb/general/putItem.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export function request(ctx) { 4 | const key = { id: util.autoId() }; 5 | const item = ctx.args.input; 6 | const condition = { id: { attributeExists: false } }; 7 | return ddb.put({ key, item, condition }); 8 | } 9 | 10 | export const response = (ctx) => ctx.result; 11 | -------------------------------------------------------------------------------- /samples/dynamodb/general/updateIncrementCount.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | const { id, ...values } = ctx.args.input; 6 | values.version = ddb.operations.increment(1); 7 | const condition = { id: { attributeExists: true } }; 8 | return ddb.update({ key: { id }, update: values, condition }); 9 | } 10 | 11 | export function response(ctx) { 12 | const { error, result } = ctx; 13 | if (error) { 14 | return util.error(error.message, error.type); 15 | } 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /samples/dynamodb/general/updateItem.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | const { id, ...values } = ctx.args.input; 6 | const condition = { id: { attributeExists: true } }; 7 | return ddb.update({ key: { id }, update: values, condition }); 8 | } 9 | 10 | export function response(ctx) { 11 | const { error, result } = ctx; 12 | if (error) { 13 | return util.error(error.message, error.type); 14 | } 15 | return result; 16 | } 17 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/all-items-today.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | /** 5 | * Queries a DynamoDB table and returns items created `today` 6 | * @param {import('@aws-appsync/utils').Context<{id: string}>} ctx the context 7 | * @returns {import('@aws-appsync/utils').DynamoDBQueryRequest} the request 8 | */ 9 | export function request(ctx) { 10 | const today = util.time.nowISO8601().substring(0, 10); 11 | return ddb.query({ 12 | query: { id: { eq: ctx.args.id }, createdAt: { beginsWith: today } }, 13 | }); 14 | } 15 | 16 | /** 17 | * Returns the query items 18 | * @param {import('@aws-appsync/utils').Context} ctx the context 19 | * @returns {[*]} a flat list of result items 20 | */ 21 | export function response(ctx) { 22 | if (ctx.error) { 23 | util.error(ctx.error.message, ctx.error.type); 24 | } 25 | return ctx.result.items; 26 | } 27 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/pagination.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | /** 5 | * Queries a DynamoDB table, limits the number of returned items, and paginates with the provided `nextToken` 6 | * @param {import('@aws-appsync/utils').Context<{id: string; limit?: number; nextToken?:string}>} ctx the context 7 | * @returns {import('@aws-appsync/utils').DynamoDBQueryRequest} the request 8 | */ 9 | export function request(ctx) { 10 | const { id, limit = 20, nextToken } = ctx.args; 11 | return ddb.query({ query: { id: { eq: id } }, limit, nextToken }); 12 | } 13 | 14 | /** 15 | * Returns the query items 16 | * @param {import('@aws-appsync/utils').Context} ctx the context 17 | * @returns {[*]} a flat list of result items 18 | */ 19 | export function response(ctx) { 20 | if (ctx.error) { 21 | util.error(ctx.error.message, ctx.error.type); 22 | } 23 | return ctx.result.items; 24 | } 25 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/simple-query.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb'; 2 | 3 | export const request = (ctx) => ddb.query({ query: { id: { eq: ctx.args.id } } }); 4 | export const response = (ctx) => ctx.result.items; 5 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/with-contains-expression.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | /** 5 | * Queries a DynamoDB table for items based on the `id` and that contain the `tag` 6 | * @param {import('@aws-appsync/utils').Context<{id: string; tag:string}>} ctx the context 7 | * @returns {import('@aws-appsync/utils').DynamoDBQueryRequest} the request 8 | */ 9 | export function request(ctx) { 10 | return ddb.query({ 11 | query: { id: { eq: ctx.args.id } }, 12 | filter: { tags: { contains: ctx.args.tag } }, 13 | }); 14 | } 15 | 16 | /** 17 | * Returns the query items 18 | * @param {import('@aws-appsync/utils').Context} ctx the context 19 | * @returns {[*]} a flat list of result items 20 | */ 21 | export function response(ctx) { 22 | if (ctx.error) { 23 | util.error(ctx.error.message, ctx.error.type); 24 | } 25 | return ctx.result.items; 26 | } 27 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/with-filter-on-index.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | return ddb.query({ 6 | index: 'name-index', 7 | query: { name: { eq: ctx.args.name } }, 8 | filter: { city: { contains: ctx.args.city } }, 9 | }); 10 | } 11 | 12 | /** 13 | * Returns the query items 14 | * @param {import('@aws-appsync/utils').Context} ctx the context 15 | * @returns {[*]} a flat list of result items 16 | */ 17 | export function response(ctx) { 18 | if (ctx.error) { 19 | util.error(ctx.error.message, ctx.error.type); 20 | } 21 | return ctx.result.items; 22 | } 23 | -------------------------------------------------------------------------------- /samples/dynamodb/queries/with-greater-than.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | /** 4 | * Queries a DynamoDB table for items created after a specified data (`createdAt`) 5 | * @param {import('@aws-appsync/utils').Context<{id: string; createdAt: string}>} ctx the context 6 | * @returns {import('@aws-appsync/utils').DynamoDBQueryRequest} the request 7 | */ 8 | export function request(ctx) { 9 | const { id, createdAt } = ctx.args; 10 | return ddb.query({ query: { id: { eq: id } }, createdAt: { gt: createdAt } }); 11 | } 12 | 13 | /** 14 | * Returns the query items 15 | * @param {import('@aws-appsync/utils').Context} ctx the context 16 | * @returns {[*]} a flat list of result items 17 | */ 18 | export function response(ctx) { 19 | if (ctx.error) { 20 | util.error(ctx.error.message, ctx.error.type); 21 | } 22 | return ctx.result.items; 23 | } 24 | -------------------------------------------------------------------------------- /samples/eventbridge/simple.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends an event to Event Bridge 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns {*} the request 7 | */ 8 | export function request(ctx) { 9 | return { 10 | operation: 'PutEvents', 11 | events: [ 12 | { 13 | source: ctx.source, 14 | detail: { 15 | key1: [1, 2, 3, 4], 16 | key2: 'strval', 17 | }, 18 | detailType: 'sampleDetailType', 19 | resources: ['Resouce1', 'Resource2'], 20 | time: util.time.nowISO8601(), 21 | }, 22 | ], 23 | }; 24 | } 25 | 26 | /** 27 | * Process the response 28 | * @param {import('@aws-appsync/utils').Context} ctx the context 29 | * @returns {*} the EventBridge response 30 | */ 31 | export function response(ctx) { 32 | return ctx.result; 33 | } 34 | -------------------------------------------------------------------------------- /samples/http/forward.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a GET request to retrieve a user's inforrmation 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns the request 7 | */ 8 | export function request(ctx) { 9 | return fetch(ctx.args.path, { 10 | // headers: util.http.copyHeaders(ctx.request.headers), 11 | body: ctx.args.body, // can include in the body 12 | query: ctx.args.query, // or in the query string 13 | }); 14 | } 15 | 16 | /** 17 | * Process the HTTP response 18 | * @param {import('@aws-appsync/utils').Context} ctx the context 19 | * @returns {*} the publish response 20 | */ 21 | export function response(ctx) { 22 | const { statusCode, body } = ctx.result; 23 | // if response is 200, return the response 24 | if (statusCode === 200) { 25 | return body; 26 | } 27 | // if response is not 200, append the response to error block. 28 | util.appendError(body, statusCode); 29 | } 30 | 31 | /** 32 | * Sends an HTTP request 33 | * @param {string} resourcePath path of the request 34 | * @param {Object} [options] values to publish 35 | * @param {'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'} [options.method] the request method 36 | * @param {Object.} [options.headers] the request headers 37 | * @param {string | Object.} [options.body] the request body 38 | * @param {Object.} [options.query] Key-value pairs that specify the query string 39 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 40 | */ 41 | function fetch(resourcePath, options) { 42 | const { method = 'GET', headers, body: _body, query = {} } = options; 43 | const [path, params] = resourcePath.split('?'); 44 | console.log('params > ', params); 45 | if (params && params.length) { 46 | params.split('&').forEach((param) => { 47 | console.log('param > ', param); 48 | const [key, value] = param.split('='); 49 | query[key] = value; 50 | }); 51 | } 52 | const body = typeof _body === 'object' ? JSON.stringify(_body) : _body; 53 | return { 54 | resourcePath: path, 55 | method, 56 | params: { headers, query, body }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /samples/http/getToApiGW.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a GET request to retrieve a user's inforrmation 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns the request 7 | */ 8 | export function request(ctx) { 9 | return fetch(`/v1/users/${ctx.args.id}`, { 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | }); 14 | } 15 | 16 | /** 17 | * Process the HTTP response 18 | * @param {import('@aws-appsync/utils').Context} ctx the context 19 | * @returns {*} the publish response 20 | */ 21 | export function response(ctx) { 22 | const { statusCode, body } = ctx.result; 23 | // if response is 200, return the response 24 | if (statusCode === 200) { 25 | return body; 26 | } 27 | // if response is not 200, append the response to error block. 28 | util.appendError(body, statusCode); 29 | } 30 | 31 | /** 32 | * Sends an HTTP request 33 | * @param {string} resourcePath path of the request 34 | * @param {Object} [options] values to publish 35 | * @param {'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'} [options.method] the request method 36 | * @param {Object.} [options.headers] the request headers 37 | * @param {string | Object.} [options.body] the request body 38 | * @param {Object.} [options.query] Key-value pairs that specify the query string 39 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 40 | */ 41 | function fetch(resourcePath, options) { 42 | const { method = 'GET', headers, body: _body, query } = options; 43 | const body = typeof _body === 'object' ? JSON.stringify(_body) : _body; 44 | return { 45 | resourcePath, 46 | method, 47 | params: { headers, query, body }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /samples/http/publishToSNS.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a publish request to the SNS topic 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns the request 7 | */ 8 | export function request(ctx) { 9 | const TOPIC_ARN = ''; 10 | const { input: values } = ctx.args; 11 | return publishToSNSRequest(TOPIC_ARN, values); 12 | } 13 | 14 | /** 15 | * Process the publish response 16 | * @param {import('@aws-appsync/utils').Context} ctx the context 17 | * @returns {string} the publish response 18 | */ 19 | export function response(ctx) { 20 | const { 21 | result: { statusCode, body }, 22 | } = ctx; 23 | if (statusCode === 200) { 24 | // if response is 200 25 | // parse the xml response 26 | return util.xml.toMap(body).PublishResponse.PublishResult; 27 | } 28 | 29 | // if response is not 200, append the response to error block. 30 | util.appendError(body, `${statusCode}`); 31 | } 32 | 33 | /** 34 | * Sends a publish request 35 | * @param {string} topicArn SNS topic ARN 36 | * @param {*} values values to publish 37 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 38 | */ 39 | function publishToSNSRequest(topicArn, values) { 40 | const arn = util.urlEncode(topicArn); 41 | const tmp = JSON.stringify(values); 42 | const message = util.urlEncode(tmp); 43 | const Body = { 44 | Action: 'Publish', 45 | Version: '2010-03-31', 46 | topicArn: arn, 47 | Message: message, 48 | }; 49 | const body = Object.entries(Body) 50 | .map(([k, v]) => `${k}=${v}`) 51 | .join('&'); 52 | 53 | return fetch('/', { 54 | method: 'POST', 55 | body, 56 | headers: { 57 | 'content-type': 'application/x-www-form-urlencoded', 58 | }, 59 | }); 60 | } 61 | 62 | /** 63 | * Sends an HTTP request 64 | * @param {string} resourcePath path of the request 65 | * @param {Object} [options] values to publish 66 | * @param {'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'} [options.method] the request method 67 | * @param {Object.} [options.headers] the request headers 68 | * @param {string | Object.} [options.body] the request body 69 | * @param {Object.} [options.query] Key-value pairs that specify the query string 70 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 71 | */ 72 | function fetch(resourcePath, options) { 73 | const { method = 'GET', headers, body: _body, query } = options; 74 | const body = typeof _body === 'object' ? JSON.stringify(_body) : _body; 75 | return { 76 | resourcePath, 77 | method, 78 | params: { headers, query, body }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /samples/http/putToApiGW.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a POST request to create a new user 5 | * @param {import('@aws-appsync/utils').Context<{input: {id: string; name:string}}>} ctx the context 6 | * @returns the request 7 | */ 8 | export function request(ctx) { 9 | return fetch(`/v1/users`, { 10 | method: 'POST', 11 | headers: { 'Content-Type': 'application/json' }, 12 | body: ctx.args.input, 13 | }); 14 | } 15 | 16 | /** 17 | * Process the HTTP response 18 | * @param {import('@aws-appsync/utils').Context} ctx the context 19 | * @returns {*} the publish response 20 | */ 21 | export function response(ctx) { 22 | const { statusCode, body } = ctx.result; 23 | // if response is 200, return the response 24 | if (statusCode === 200) { 25 | return body; 26 | } 27 | // if response is not 200, append the response to error block. 28 | util.appendError(body, statusCode); 29 | } 30 | 31 | /** 32 | * Sends an HTTP request 33 | * @param {string} resourcePath path of the request 34 | * @param {Object} [options] values to publish 35 | * @param {'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'} [options.method] the request method 36 | * @param {Object.} [options.headers] the request headers 37 | * @param {string | Object.} [options.body] the request body 38 | * @param {Object.} [options.query] Key-value pairs that specify the query string 39 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 40 | */ 41 | function fetch(resourcePath, options) { 42 | const { method = 'GET', headers, body: _body, query } = options; 43 | const body = typeof _body === 'object' ? JSON.stringify(_body) : _body; 44 | return { 45 | resourcePath, 46 | method, 47 | params: { headers, query, body }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /samples/http/translate.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a tranlsate request to the Translate service 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns the request 7 | */ 8 | export function request(ctx) { 9 | const { text = 'Hello World!', source = 'EN', target = 'FR' } = ctx.args; 10 | return awsTranslateRequest(text, source, target); 11 | } 12 | 13 | /** 14 | * Process the translate response 15 | * @param {import('@aws-appsync/utils').Context} ctx the context 16 | * @returns {string} the translated text 17 | */ 18 | export function response(ctx) { 19 | const { result } = ctx; 20 | if (result.statusCode !== 200) { 21 | return util.appendError(result.body, `${result.statusCode}`); 22 | } 23 | const body = JSON.parse(result.body); 24 | return body.TranslatedText; 25 | } 26 | 27 | /** 28 | * Sends a request to the Translate service 29 | * @param {string} text text to translate 30 | * @param {string} source the source language code 31 | * @param {string} target the target language code 32 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 33 | */ 34 | function awsTranslateRequest(text, source, target) { 35 | return fetch('/', { 36 | method: 'POST', 37 | headers: { 38 | 'content-type': 'application/x-amz-json-1.1', 39 | 'x-amz-target': 'AWSShineFrontendService_20170701.TranslateText', 40 | }, 41 | body: { Text: text, SourceLanguageCode: source, TargetLanguageCode: target }, 42 | }); 43 | } 44 | 45 | /** 46 | * Sends an HTTP request 47 | * @param {string} resourcePath path of the request 48 | * @param {Object} [options] values to publish 49 | * @param {'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'} [options.method] the request method 50 | * @param {Object.} [options.headers] the request headers 51 | * @param {string | Object.} [options.body] the request body 52 | * @param {Object.} [options.query] Key-value pairs that specify the query string 53 | * @returns {import('@aws-appsync/utils').HTTPRequest} the request 54 | */ 55 | function fetch(resourcePath, options) { 56 | const { method = 'GET', headers, body: _body, query } = options; 57 | const body = typeof _body === 'object' ? JSON.stringify(_body) : _body; 58 | return { 59 | resourcePath, 60 | method, 61 | params: { headers, query, body }, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /samples/lambda/invoke.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Sends a request to a Lambda function. Passes all information about the request from the `info` object. 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns {import('@aws-appsync/utils').LambdaRequest} the request 7 | */ 8 | export function request(ctx) { 9 | const payload = { 10 | arguments: ctx.arguments, 11 | identity: ctx.identity, 12 | source: ctx.source, 13 | request: ctx.request, 14 | info: { 15 | fieldName: ctx.info.fieldName, 16 | parentTypeName: ctx.info.parentTypeName, 17 | variables: ctx.info.variables, 18 | selectionSetList: ctx.info.selectionSetList, 19 | selectionSetGraphQL: ctx.info.selectionSetGraphQL, 20 | }, 21 | }; 22 | return { operation: 'Invoke', payload }; 23 | } 24 | 25 | /** 26 | * Process a Lambda function response 27 | * @param {import('@aws-appsync/utils').Context} ctx the context 28 | * @returns {*} the Lambda function response 29 | */ 30 | export function response(ctx) { 31 | const { result, error } = ctx; 32 | if (error) { 33 | util.error(error.message, error.type, result); 34 | } 35 | return result; 36 | } 37 | -------------------------------------------------------------------------------- /samples/opensearch/geo.js: -------------------------------------------------------------------------------- 1 | // TITLE: Get all documents within a 20 mile radius 2 | 3 | import { util } from '@aws-appsync/utils'; 4 | 5 | /** 6 | * Searches for all documents using Geodistance aggregation 7 | * @param {import('@aws-appsync/utils').Context} ctx the context 8 | * @returns {*} the request 9 | */ 10 | export function request(ctx) { 11 | // Replace with actual values, e.g.: post 12 | const index = ''; 13 | return { 14 | operation: 'GET', 15 | path: `/${index}/_search`, 16 | params: { 17 | body: { 18 | query: { 19 | filtered: { 20 | query: { match_all: {} }, 21 | filter: { 22 | geo_distance: { 23 | distance: '20mi', 24 | location: { lat: 47.6205, lon: 122.3493 }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }; 32 | } 33 | 34 | /** 35 | * Returns the fetched items 36 | * @param {import('@aws-appsync/utils').Context} ctx the context 37 | * @returns {*} the result 38 | */ 39 | export function response(ctx) { 40 | if (ctx.error) { 41 | util.error(ctx.error.message, ctx.error.type); 42 | } 43 | return ctx.result.hits.hits.map((hit) => hit._source); 44 | } 45 | -------------------------------------------------------------------------------- /samples/opensearch/getDocumentByID.js: -------------------------------------------------------------------------------- 1 | // TITLE: Get document by id 2 | 3 | import { util } from '@aws-appsync/utils'; 4 | 5 | /** 6 | * Gets a document by `id` 7 | * @param {import('@aws-appsync/utils').Context} ctx the context 8 | * @returns {*} the request 9 | */ 10 | export function request(ctx) { 11 | // Replace with actual values, e.g.: post 12 | const index = ''; 13 | return { 14 | operation: 'GET', 15 | path: `/${index}/_doc/${ctx.args.id}`, 16 | }; 17 | } 18 | 19 | /** 20 | * Returns the fetched item 21 | * @param {import('@aws-appsync/utils').Context} ctx the context 22 | * @returns {*} the result 23 | */ 24 | export function response(ctx) { 25 | if (ctx.error) { 26 | util.error(ctx.error.message, ctx.error.type); 27 | } 28 | return ctx.result['_source']; 29 | } 30 | -------------------------------------------------------------------------------- /samples/opensearch/paginate.js: -------------------------------------------------------------------------------- 1 | // TITLE: Paginate with fixed-size pages 2 | 3 | import { util } from '@aws-appsync/utils'; 4 | 5 | /** 6 | * Paginates through search results using `from` and `size` arguments 7 | * @param {import('@aws-appsync/utils').Context} ctx the context 8 | * @returns {*} the request 9 | */ 10 | export function request(ctx) { 11 | // Replace with actual values, e.g.: post 12 | const index = ''; 13 | return { 14 | operation: 'GET', 15 | path: `/${index}/_search`, 16 | params: { 17 | body: { 18 | from: ctx.args.from ?? 0, 19 | size: ctx.args.size ?? 50, 20 | }, 21 | }, 22 | }; 23 | } 24 | 25 | /** 26 | * Returns the fetched items 27 | * @param {import('@aws-appsync/utils').Context} ctx the context 28 | * @returns {*} the result 29 | */ 30 | export function response(ctx) { 31 | if (ctx.error) { 32 | util.error(ctx.error.message, ctx.error.type); 33 | } 34 | return ctx.result.hits.hits.map((hit) => hit._source); 35 | } 36 | -------------------------------------------------------------------------------- /samples/opensearch/simpleTermQuery.js: -------------------------------------------------------------------------------- 1 | // TITLE: Simple search 2 | // (default for OS Function) 3 | 4 | import { util } from '@aws-appsync/utils'; 5 | 6 | /** 7 | * Searches for documents by using an input term 8 | * @param {import('@aws-appsync/utils').Context} ctx the context 9 | * @returns {*} the request 10 | */ 11 | export function request(ctx) { 12 | // Replace with actual values, e.g.: post 13 | const index = ''; 14 | return { 15 | operation: 'GET', 16 | path: `/${index}/_search`, 17 | params: { 18 | body: { 19 | from: 0, 20 | size: 50, 21 | query: { 22 | term: { 23 | '': ctx.args.field, // replace with your field 24 | }, 25 | }, 26 | }, 27 | }, 28 | }; 29 | } 30 | 31 | /** 32 | * Returns the fetched items 33 | * @param {import('@aws-appsync/utils').Context} ctx the context 34 | * @returns {*} the result 35 | */ 36 | export function response(ctx) { 37 | if (ctx.error) { 38 | util.error(ctx.error.message, ctx.error.type); 39 | } 40 | return ctx.result.hits.hits.map((hit) => hit._source); 41 | } 42 | -------------------------------------------------------------------------------- /samples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "samples", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@aws-appsync/utils": "^1.3.1" 13 | } 14 | }, 15 | "node_modules/@aws-appsync/utils": { 16 | "version": "1.3.1", 17 | "resolved": "https://registry.npmjs.org/@aws-appsync/utils/-/utils-1.3.1.tgz", 18 | "integrity": "sha512-wJdB1de0t5FxU1rnIIcSkbvMrONjuRpRegAcGbReClxoqajiR+U1PP0mylHgp9X3UTMe372wkuDFFABjTXqS0g==", 19 | "dev": true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@aws-appsync/utils": "^1.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/pipeline/default.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Triggers the pipeline 5 | * @param {import('@aws-appsync/utils').Context} ctx the context 6 | * @returns an object that is send to the first function 7 | */ 8 | export function request(ctx) { 9 | return {}; 10 | } 11 | 12 | /** 13 | * Simply forwards the result 14 | * @param {import('@aws-appsync/utils').Context} ctx the context 15 | * @returns {*} the result from the last function in the pipeline 16 | */ 17 | export const response = (ctx) => ctx.prev.result; 18 | -------------------------------------------------------------------------------- /samples/rds/README.md: -------------------------------------------------------------------------------- 1 | 2 | # AppSyncJS built-in module for Amazon RDS 3 | 4 | 5 | - [AppSyncJS built-in module for Amazon RDS](#appsyncjs-built-in-module-for-amazon-rds) 6 | - [Functions](#functions) 7 | - [sql](#sql) 8 | - [Select](#select) 9 | - [Basic use](#basic-use) 10 | - [Specifying columns](#specifying-columns) 11 | - [Limits and offsets](#limits-and-offsets) 12 | - [Order By](#order-by) 13 | - [Filters](#filters) 14 | - [Joins](#joins) 15 | - [Aggregates](#aggregates) 16 | - [More on aliases](#more-on-aliases) 17 | - [Subqueries](#subqueries) 18 | - [Insert](#insert) 19 | - [Single item insertions](#single-item-insertions) 20 | - [MySQL use case](#mysql-use-case) 21 | - [Postgres use case](#postgres-use-case) 22 | - [Update](#update) 23 | - [Casting](#casting) 24 | 25 | 26 | AppSync's built-in module for Amazon RDS module provides an enhanced experience for interacting with Amazon Aurora databases configured with the Amazon RDS Data API. The module is imported using `@aws-appsync/utils/rds`: 27 | 28 | ```js 29 | import * as rds from '@aws-appsync/utils/rds'; 30 | ``` 31 | 32 | Functions can also be imported individually. For instance, the import below uses sql: 33 | 34 | ```js 35 | import { sql } from '@aws-appsync/utils/rds'; 36 | ``` 37 | 38 | The example in this folder are based on the `Chinook_PostgreSql.sql` database schema that you can find [here](https://github.com/lerocha/chinook-database/blob/master/ChinookDatabase/DataSources/Chinook_PostgreSql.sql). You can load this schema in your database and create an AppSync GraphQL API from the definition from the AppSync console. Learn more about the [introspection feature](https://docs.aws.amazon.com/appsync/latest/devguide/rds-introspection.html#using-introspection-console). 39 | 40 | ## Functions 41 | 42 | You can use the AWS AppSync RDS module's utility helpers to interact with your database. 43 | 44 | ### sql 45 | 46 | The `sql` tag template utility is your go-to tool to write SQL queries directly. Use this utility when the provided utilities below are not enough to create the statements that you need. You can also use the `sql` operator to customize certain fields of the `select`, `insert`, `update`, and `remove` utilities. 47 | 48 | ```js 49 | import { sql, createPgStatement as pg } from '@aws-appsync/utils/rds'; 50 | export function request(ctx) { 51 | return pg(sql`select count(*) from album where artist_id = ${ctx.args.artist_id}`) 52 | } 53 | ``` 54 | 55 | This will generate a query and automatically map dynamic values to placeholders. This approach is safer than writing queries directly and helps prevent potential SQL Injection vulnerabilities. 56 | 57 | In your logs, you see the following request. 58 | 59 | ```JSON 60 | { 61 | "statements": [ 62 | "select count(*) from album where artist_id = :P0" 63 | ], 64 | "variableMap": { 65 | ":P0": 134 66 | }, 67 | "variableTypeHintMap": {} 68 | } 69 | ``` 70 | 71 | ### Select 72 | 73 | the select utility creates a `select` statement to query your relational database. 74 | 75 | #### Basic use 76 | 77 | In its basic form, you can specify the table you want to query: 78 | 79 | ```js 80 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 81 | export function request(ctx) { 82 | // Generates statement: 83 | // "SELECT * FROM "album" 84 | return pg(select({table: 'album'})); 85 | } 86 | ``` 87 | 88 | Note that you can also specify the schema in your table identifier: 89 | 90 | ```js 91 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 92 | export function request(ctx) { 93 | // Generates statement: 94 | // SELECT * FROM "private"."album" 95 | return pg(select({table: 'private.album'})); 96 | } 97 | ``` 98 | 99 | And you can specify an alias as well 100 | 101 | ```js 102 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 103 | export function request(ctx) { 104 | // Generates statement: 105 | // SELECT * FROM "private"."album" as "al" 106 | return pg(select({table: {al: 'private.album'})); 107 | } 108 | 109 | ``` 110 | 111 | Handling the return 112 | 113 | You can return a list of items using the `toJsonObject` helper: 114 | 115 | ```javascript 116 | import { toJsonObject } from '@aws-appsync/utils/rds' 117 | export function response(ctx) { 118 | const { error, result } = ctx 119 | if (error) { 120 | return util.appendError(error.message, error.type, result) 121 | } 122 | return toJsonObject(result)[0] 123 | } 124 | ``` 125 | 126 | To return a specific item, simply select an index from the array: 127 | 128 | ```javascript 129 | import { toJsonObject } from '@aws-appsync/utils/rds' 130 | export function response(ctx) { 131 | const { error, result } = ctx 132 | if (error) { 133 | return util.appendError(error.message, error.type, result) 134 | } 135 | return toJsonObject(result)[0][0] 136 | } 137 | ``` 138 | 139 | #### Specifying columns 140 | 141 | You can specify columns with the columns property. If this isn't set to a value, it defaults to `*`: 142 | 143 | ```js 144 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 145 | export function request(ctx) { 146 | // Generates statement: 147 | // SELECT "id", "name" 148 | // FROM "album" 149 | return pg(select({ 150 | table: 'album', 151 | columns: ['album_id', 'title'] 152 | })); 153 | } 154 | ``` 155 | 156 | You can specify a column's table as well: 157 | 158 | ```js 159 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 160 | export function request(ctx) { 161 | // Generates statement: 162 | // SELECT "id", "album"."name" 163 | // FROM "album" 164 | return pg(select({ 165 | table: 'album', 166 | columns: ['album_id', 'album.title'] 167 | })); 168 | } 169 | ``` 170 | 171 | You can use aliases 172 | 173 | ```js 174 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 175 | export function request(ctx) { 176 | // Generates statement: 177 | // SELECT "id", "album"."title" as "name" 178 | // FROM "album" 179 | return pg(select({ 180 | table: 'album', 181 | columns: ['album_id', { name: 'album.title' }] 182 | })); 183 | } 184 | ``` 185 | 186 | #### Limits and offsets 187 | 188 | You can apply limit and offset to the query: 189 | 190 | ```js 191 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 192 | export function request(ctx) { 193 | // Generates statement: 194 | // SELECT "id", "name" 195 | // FROM "album" 196 | // LIMIT :limit 197 | // OFFSET :offset 198 | return pg(select({ table: 'album', limit: 10, offset: 40 })); 199 | } 200 | ``` 201 | 202 | #### Order By 203 | 204 | You can sort your results with the `orderBy` property. Provide an array of objects specifying the column and an optional `dir` property: 205 | 206 | ```js 207 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 208 | export function request(ctx) { 209 | return pg(select({ 210 | table: 'album', 211 | columns: ['album_id', 'artist_id', 'title'], 212 | orderBy: [{column: 'artist_id'}, {column: 'title', dir: 'DESC'}] 213 | })); 214 | } 215 | ``` 216 | 217 | #### Filters 218 | 219 | You can build filters by using the special condition object: 220 | 221 | ```js 222 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds' 223 | export function request(ctx) { 224 | return pg(select({ 225 | table: 'album', 226 | columns: ['album_id', 'artist_id', 'title'], 227 | where: {title: {beginsWith: 'W'}} 228 | })); 229 | } 230 | ``` 231 | 232 | You can also combine filters: 233 | 234 | ```js 235 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds' 236 | export function request(ctx) { 237 | return pg(select({ 238 | table: 'track', 239 | columns: ['track_id', 'album_id', 'milliseconds'], 240 | where: {album_id: {between: [1,2]}, milliseconds: {gt: 100_000}} 241 | })); 242 | } 243 | ``` 244 | 245 | You can also create OR statements: 246 | 247 | ```js 248 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds' 249 | export function request(ctx) { 250 | return pg(select({ 251 | table: 'track', 252 | columns: ['track_id', 'name'], 253 | where: { or: [ 254 | { unit_price: { lt: 1} }, 255 | { composer: { attributeExists: false } } 256 | ]} 257 | })); 258 | } 259 | ``` 260 | 261 | You can also negate a condition with not: 262 | 263 | ```js 264 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds' 265 | export function request(ctx) { 266 | return createPgStatement(select({ 267 | table: 'track', 268 | columns: ['track_id', 'name'], 269 | where: { not: [ 270 | {or: [ 271 | { unit_price: { lt: 1} }, 272 | { composer: { attributeExists: false } } 273 | ]} 274 | ]} 275 | })); 276 | } 277 | ``` 278 | 279 | You can use the following operators to compare values: 280 | 281 | | Operator | Description | Possible values | 282 | | --------------- | --------------- | --------------- | 283 | | eq | Equal | number, string, boolean | 284 | | ne | Not equal | number, string, boolean | 285 | | le | Less than or equal | number, string | 286 | | lt | Less than | number, string | 287 | | ge | Greater than or equal | number, string | 288 | | gt | Greater than | number, string | 289 | | contains | Like | string | 290 | | notContains | Not like | string | 291 | | beginsWith | Starts with prefix | string | 292 | | between | Between two values | number, string | 293 | | attributeExists | The attribute is not null | number, string, boolean | 294 | | size | checks the length of the element | string | 295 | 296 | You can use the `sql` helper to write custom conditions: 297 | 298 | ```javascript 299 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds'; 300 | export function request(ctx) { 301 | return pg(select({ 302 | from: 'album', 303 | where: sql`length(title) > ${ctx.args.size}` 304 | })) 305 | } 306 | ``` 307 | 308 | #### Joins 309 | 310 | You can use join in you select statements. 311 | 312 | ```js 313 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds'; 314 | export function request(ctx) { 315 | return pg(select({ 316 | from: 'album', 317 | join: [{from: 'artist', using: ['artist_id']}] 318 | })) 319 | } 320 | ``` 321 | 322 | Note: use `using` when both sides of the join use the same name for the joining column(s). To specify you custom condition, use `on` with the `sql` util. 323 | 324 | ```js 325 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds'; 326 | export function request(ctx) { 327 | return pg(select({ 328 | from: 'album', 329 | join: [{from: 'artist', on: sql`album.some_id = artist.another_id`}] 330 | })) 331 | } 332 | ``` 333 | 334 | The following join expressions are supported 335 | 336 | - join 337 | - innerJoin 338 | - leftJoin 339 | - leftOuterJoin 340 | - rightJoin 341 | - rightOuterJoin 342 | - fullOuterJoin - Postgres only 343 | - crossJoin 344 | - joinNatural 345 | - innerJoinNatural 346 | - leftJoinNatural 347 | - leftOuterJoinNatural 348 | - rightJoinNatural 349 | - rightOuterJoinNatural 350 | - fullOuterJoinNatural - Postgres only 351 | 352 | #### Aggregates 353 | 354 | With AppSync, you can do aggregations using the following functions: `min`, `minDistinct` , `max` , `maxDistinct` , `sum` , `sumDistinct` , `avg` , `avgDistinct` , `count` , `countDistinct`. When using aggregations, you can make use of `groupBy` and `having` 355 | 356 | To count the rows in a result: Get the number of albums for every artist with a minimum of 5 albums. 357 | 358 | ```js 359 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds'; 360 | export function request(ctx) { 361 | return pg(select({ 362 | table : 'album', 363 | columns: ['artist_id', {count: agg.count('*')}], 364 | groupBy: ['artist_id'], 365 | having: { 366 | album_id: { 367 | count: {ge: 5} 368 | } 369 | } 370 | })) 371 | } 372 | ``` 373 | 374 | #### More on aliases 375 | 376 | You can leverage aliases in your queries. Aliases are supported on the `table`, `from`, `columns` and `using` properties. 377 | 378 | ```js 379 | import { select, createPgStatement as pg } from '@aws-appsync/utils/rds'; 380 | export function request(ctx) { 381 | return pg(select({ 382 | table : {record: 'album' }, 383 | columns: ['id', {name: 'title'}] 384 | })); 385 | } 386 | ``` 387 | 388 | #### Subqueries 389 | 390 | You can use subqueries in your select statement by leveraging the `from` property. `from` works like `table`, but supports strings, aliases, `sql` and `select`! 391 | 392 | ```javascript 393 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds'; 394 | export function request(ctx) { 395 | 396 | // First, fetch all the albums that have more than 1 genre in their tracklist 397 | const sub = select({ 398 | from: 'album', 399 | columns: [ 400 | 'album_id', 'title', 'artist_id', 401 | {tracks: agg.count('track_id')}, 402 | {genres: agg.countDistinct('genre_id')} 403 | ], 404 | join: [{from: 'track', using: ['album_id']}], 405 | groupBy: [1], // you can use ordinal in the groupBy close 406 | having: { 407 | 'genre_id': { 408 | countDistinct: {gt: 1} 409 | } 410 | }, 411 | orderBy: [{column: 'genres', dir: 'desc'}] 412 | }) 413 | 414 | // next, use the subquery and retrieve the name of the artist for those albums 415 | return pg(select({ 416 | from: { sub }, // an identifier or an alias. 417 | columns: ['album_id', 'title', 'name'], 418 | join: [{from: 'artist', using: ['artist_id']}] 419 | })) 420 | } 421 | ``` 422 | 423 | ### Insert 424 | 425 | The insert utility provides a straightforward way of inserting single row items in your database with the INSERT operation. 426 | 427 | #### Single item insertions 428 | 429 | To insert an item, specify the table and then pass in your object of values. The object keys are mapped to your table columns. Columns names are automatically escaped, and values are sent to the database using the variable map: 430 | 431 | ```js 432 | import { insert, createMySQLStatement as mysql } from '@aws-appsync/utils/rds'; 433 | export function request(ctx) { 434 | // Generates statement: 435 | // INSERT INTO `album`(`title`, `artist_id`) 436 | // VALUES(:title, :artist_id) 437 | return mysql(insert({ table: 'album', values: ctx.args.input })) 438 | } 439 | ``` 440 | 441 | #### MySQL use case 442 | 443 | You can combine an insert followed by a select to retrieve your inserted row: 444 | 445 | ```js 446 | import { insert, select, createMySQLStatement as mysql } from '@aws-appsync/utils/rds'; 447 | export function request(ctx) { 448 | const { input: values } = ctx.args; 449 | const insertStatement = insert({ table: 'album', values }); 450 | const selectStatement = select({ 451 | table: 'album', 452 | columns: '*', 453 | where: { id: { eq: values.id } }, 454 | limit: 1, 455 | }); 456 | 457 | // Generates statement: 458 | // INSERT INTO `album`(`album_id`, `title`) 459 | // VALUES(:ALBUM_ID, :TITLE) 460 | // and 461 | // SELECT * 462 | // FROM `album` 463 | // WHERE `album_id` = :ALBUM_ID 464 | return mysql(insertStatement, selectStatement) 465 | } 466 | ``` 467 | 468 | #### Postgres use case 469 | 470 | With Postgres, you can use returning 471 | 472 | to obtain data from the row that you inserted. It accepts * or an array of column names: 473 | 474 | ```js 475 | import { insert, createPgStatement as pg } from '@aws-appsync/utils/rds'; 476 | export function request(ctx) { 477 | const { input: values } = ctx.args; 478 | const statement = insert({ 479 | table: 'album', 480 | values, 481 | returning: '*' 482 | }); 483 | return pg(statement) 484 | } 485 | ``` 486 | 487 | ### Update 488 | 489 | The update utility allows you to update existing rows. You can use the condition object to apply changes to the specified columns in all the rows that satisfy the condition. For example, let's say we have a schema that allows us to make this mutation. We want to update the name of Person with the id value of 3 but only if we've known them (known_since) since the year 2000: 490 | 491 | ```graphql 492 | mutation Update { 493 | updatePerson( 494 | input: {id: 3, name: "Jon"}, 495 | condition: {known_since: {ge: "2000"}} 496 | ) { 497 | id 498 | name 499 | } 500 | } 501 | ``` 502 | 503 | Our update resolver looks like this: 504 | 505 | ```js 506 | import { update, createPgStatement as pg } from '@aws-appsync/utils/rds'; 507 | export function request(ctx) { 508 | const { input: { id, ...values }, condition } = ctx.args; 509 | const where = { ...condition, id: { eq: id } }; 510 | const statement = update({ 511 | table: 'persons', 512 | values, 513 | where, 514 | returning: ['id', 'name'], 515 | }); 516 | 517 | // Generates statement: 518 | // UPDATE "persons" 519 | // SET "name" = :NAME, "birthday" = :BDAY, "country" = :COUNTRY 520 | // WHERE "id" = :ID 521 | // RETURNING "id", "name" 522 | return pg(statement) 523 | } 524 | ``` 525 | 526 | We can add a check to our condition to make sure that only the row that has the primary key id equal to 3 is updated. Similarly, for Postgres inserts, you can use returning to return the modified data. 527 | Remove 528 | 529 | The remove utility allows you to delete existing rows. You can use the condition object on all rows that satisfy the condition. Note that delete is a reserved keyword in JavaScript. remove should be used instead: 530 | 531 | ```js 532 | import { remove, createPgStatement as pg } from '@aws-appsync/utils/rds'; 533 | export function request(ctx) { 534 | const { input: { id }, condition } = ctx.args; 535 | const where = { ...condition, id: { eq: id } }; 536 | const statement = remove({ 537 | table: 'persons', 538 | where, 539 | returning: ['id', 'name'], 540 | }); 541 | 542 | // Generates statement: 543 | // DELETE "persons" 544 | // WHERE "id" = :ID 545 | // RETURNING "id", "name" 546 | return pg(statement) 547 | } 548 | ``` 549 | 550 | ### Casting 551 | 552 | In some cases, you may want more specificity about the correct object type to use in your statement. You can use the provided type hints to specify the type of your parameters. AWS AppSync supports the same type hints as the Data API. You can cast your parameters by using the `typeHint` functions from the AWS AppSync rds module. 553 | 554 | The following example allows you to send an array as a value that is casted as a JSON object. We use the -> operator to retrieve the element at the index 2 in the JSON array: 555 | 556 | ```js 557 | import { sql, createPgStatement as pg, toJsonObject, typeHint } from '@aws-appsync/utils/rds'; 558 | 559 | export function request(ctx) { 560 | const arr = ctx.args.list_of_ids 561 | const statement = sql`select ${typeHint.JSON(arr)}->2 as value` 562 | return pg(statement) 563 | } 564 | 565 | export function response(ctx) { 566 | return toJsonObject(ctx.result)[0][0].value 567 | } 568 | ``` 569 | 570 | Casting is also useful when handling and comparing DATE, TIME, and TIMESTAMP: 571 | 572 | ```js 573 | import { select, createPgStatement as pg, typeHint } from '@aws-appsync/utils/rds'; 574 | export function request(ctx) { 575 | const when = ctx.args.when 576 | const statement = select({ 577 | table: 'persons', 578 | where: { createdAt : { gt: typeHint.DATETIME(when) } } 579 | }) 580 | return pg(statement) 581 | } 582 | ``` 583 | 584 | Here's another example showing how you can send the current date and time: 585 | 586 | ```js 587 | import { sql, createPgStatement as pg, typeHint } from '@aws-appsync/utils/rds'; 588 | 589 | export function request(ctx) { 590 | const now = util.time.nowFormatted('YYYY-MM-dd HH:mm:ss') 591 | return createPgStatement(sql`select ${typeHint.TIMESTAMP(now)}`) 592 | } 593 | ``` 594 | 595 | Available type hints 596 | 597 | - `typeHint.DATE` - The corresponding parameter is sent as an object of the DATE type to the database. The accepted format is YYYY-MM-DD. 598 | - `typeHint.DECIMAL` - The corresponding parameter is sent as an object of the DECIMAL type to the database. 599 | - `typeHint.JSON` - The corresponding parameter is sent as an object of the JSON type to the database. 600 | - `typeHint.TIME` - The corresponding string parameter value is sent as an object of the TIME type to the database. The accepted format is HH:MM:SS[.FFF]. 601 | - `typeHint.TIMESTAMP` - The corresponding string parameter value is sent as an object of the TIMESTAMP type to the database. The accepted format is YYYY-MM-DD HH:MM:SS[.FFF]. 602 | - `typeHint.UUID` - The corresponding string parameter value is sent as an object of the UUID type to the database. 603 | -------------------------------------------------------------------------------- /samples/rds/queries/invoice.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { 3 | select, 4 | toJsonObject, 5 | createPgStatement as pg, 6 | agg, 7 | typeHint as th, 8 | } from '@aws-appsync/utils/rds' 9 | 10 | export function request(ctx) { 11 | const query = select({ 12 | from: { i: 'invoice' }, 13 | columns: [agg.count('i.invoice_id'), agg.sum('i.total')], 14 | where: { 15 | invoice_date: { 16 | between: [th.TIMESTAMP('2021-01-01 00:00:00'), th.TIMESTAMP('2022-12-31 00:00:00')], 17 | }, 18 | }, 19 | }) 20 | return pg(query) 21 | } 22 | 23 | export function response(ctx) { 24 | const { error, result } = ctx 25 | if (error) { 26 | return util.appendError(error.message, error.type, result) 27 | } 28 | return toJsonObject(result)[0] 29 | } 30 | -------------------------------------------------------------------------------- /samples/rds/queries/invoices.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { select, toJsonObject, createPgStatement as pg } from '@aws-appsync/utils/rds' 3 | 4 | export function request(ctx) { 5 | const query = select({ 6 | columns: ['i.invoice_line_id', { track: 't.name' }, { artist: 'ar.name' }], 7 | from: { i: 'invoice_line' }, 8 | join: [ 9 | { from: { t: 'track' }, using: ['track_id'] }, 10 | { from: { al: 'album' }, using: ['album_id'] }, 11 | { from: { ar: 'artist' }, using: ['artist_id'] }, 12 | ], 13 | }) 14 | return pg(query) 15 | } 16 | 17 | export function response(ctx) { 18 | const { error, result } = ctx 19 | if (error) { 20 | return util.appendError(error.message, error.type, result) 21 | } 22 | return toJsonObject(result)[0] 23 | } 24 | -------------------------------------------------------------------------------- /samples/rds/queries/playlist_count.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { 3 | select, 4 | toJsonObject, 5 | createPgStatement as pg, 6 | agg, 7 | typeHint as th, 8 | } from '@aws-appsync/utils/rds' 9 | 10 | // -- 16. Provide a query that shows all the Tracks, but displays no IDs. The resultant table should include the Album name, Media type and Genre. 11 | export function request(ctx) { 12 | const query = select({ 13 | columns: [ 14 | { track: 't.name' }, 15 | 't.composer', 16 | 't.milliseconds', 17 | 't.bytes', 18 | 't.unit_price', 19 | { album: 'a.title' }, 20 | { genre: 'g.name' }, 21 | { 'media type': 'm.name' }, 22 | ], 23 | from: { t: 'track' }, 24 | join: [ 25 | { from: { a: 'album' }, using: ['album_id'] }, 26 | { from: { g: 'genre' }, using: ['genre_id'] }, 27 | { from: { m: 'media_type' }, using: ['media_type_id'] }, 28 | ], 29 | }) 30 | return pg(query) 31 | } 32 | 33 | export function response(ctx) { 34 | const { error, result } = ctx 35 | if (error) { 36 | return util.appendError(error.message, error.type, result) 37 | } 38 | return toJsonObject(result)[0] 39 | } 40 | -------------------------------------------------------------------------------- /samples/rds/queries/sales.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { 3 | select, 4 | toJsonObject, 5 | createPgStatement as pg, 6 | agg, 7 | typeHint as th, 8 | } from '@aws-appsync/utils/rds' 9 | 10 | export function request(ctx) { 11 | const query = select({ 12 | columns: [{ total: agg.max('total') }], 13 | from: { t: 'track' }, 14 | join: [ 15 | { from: { a: 'album' }, using: ['album_id'] }, 16 | { from: { g: 'genre' }, using: ['genre_id'] }, 17 | { from: { m: 'media_type' }, using: ['media_type_id'] }, 18 | ], 19 | }) 20 | return pg(query) 21 | } 22 | 23 | export function response(ctx) { 24 | const { error, result } = ctx 25 | if (error) { 26 | return util.appendError(error.message, error.type, result) 27 | } 28 | return toJsonObject(result)[0] 29 | } 30 | -------------------------------------------------------------------------------- /samples/rds/queries/select.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { select, toJsonObject, createPgStatement as pg, agg } from '@aws-appsync/utils/rds' 3 | 4 | export function request(ctx) { 5 | const query = select({ 6 | from: 'album', 7 | columns: ['name', { count: agg.count('*') }], 8 | join: [{ from: 'artist', using: ['artist_id'] }], 9 | groupBy: ['name'], 10 | having: { 11 | album_id: { count: { gt: 1 } }, 12 | }, 13 | orderBy: [{ column: 'name' }], 14 | }) 15 | return pg(query) 16 | } 17 | 18 | export function response(ctx) { 19 | const { error, result } = ctx 20 | if (error) { 21 | return util.appendError(error.message, error.type, result) 22 | } 23 | return toJsonObject(result)[0] 24 | } 25 | -------------------------------------------------------------------------------- /samples/rds/queries/sql.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import { sql, toJsonObject, createPgStatement as pg } from '@aws-appsync/utils/rds' 3 | 4 | export function request(ctx) { 5 | return pg(sql`select count(*) from album where artist_id = ${ctx.args.artist_id}`) 6 | } 7 | 8 | export function response(ctx) { 9 | const { error, result } = ctx 10 | if (error) { 11 | return util.appendError(error.message, error.type, result) 12 | } 13 | return toJsonObject(result)[0][0] 14 | } 15 | -------------------------------------------------------------------------------- /samples/rds/queries/subquery.js: -------------------------------------------------------------------------------- 1 | import { select, createPgStatement as pg, agg } from '@aws-appsync/utils/rds' 2 | export function request(ctx) { 3 | // First, fetch all the albums that have more than 1 genre in their tracklist 4 | const sub = select({ 5 | from: 'album', 6 | columns: [ 7 | 'album_id', 8 | 'title', 9 | 'artist_id', 10 | { tracks: agg.count('track_id') }, 11 | { genres: agg.countDistinct('genre_id') }, 12 | ], 13 | join: [{ from: 'track', using: ['album_id'] }], 14 | groupBy: [1], // you can use ordinal in the groupBy close 15 | having: { 16 | genre_id: { 17 | countDistinct: { gt: 1 }, 18 | }, 19 | }, 20 | orderBy: [{ column: 'genres', dir: 'desc' }], 21 | }) 22 | 23 | // next, use the subquery and retrieve the name of the artist for those albums 24 | return pg( 25 | select({ 26 | from: { sub }, // an identifier or an alias. 27 | columns: ['album_id', 'title', 'name'], 28 | join: [{ from: 'artist', using: ['artist_id'] }], 29 | }), 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/evaluate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit -o nounset -o pipefail 3 | shopt -s nullglob 4 | 5 | script_path=$( 6 | cd "$(dirname "${BASH_SOURCE[0]}")" 7 | pwd -P 8 | ) 9 | 10 | 11 | node "$script_path/evaluate/index.mjs" "$@" -------------------------------------------------------------------------------- /scripts/evaluate/index.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { argv } from 'node:process'; 3 | import { program } from 'commander'; 4 | import { readFileSync } from 'fs'; 5 | 6 | program 7 | .argument('') 8 | .option('-f, --fn ', 'the function to evaluate', 'request') 9 | .option('-c, --context ', 'your context') 10 | .option('-d, --debug', 'debug mode') 11 | .action((resolver, { fn = 'request', context, debug }) => { 12 | try { 13 | const ctx = !context ? '{}' : `file://${context}`; 14 | const RUNTIME = 'name=APPSYNC_JS,runtimeVersion=1.0.0'; 15 | const _fn = 16 | fn === 'req' || fn === 'request' 17 | ? 'request' 18 | : fn === 'res' || fn === 'response' 19 | ? 'response' 20 | : 'unknown'; 21 | 22 | if (_fn === 'unknown') { 23 | console.error('unknown `fn` value: ', fn); 24 | return; 25 | } 26 | const buffer = execSync( 27 | `aws appsync evaluate-code --code file://${resolver} --context ${ctx} --function ${_fn} --runtime ${RUNTIME}` 28 | ); 29 | const json = JSON.parse(buffer.toString()); 30 | if (debug) { 31 | console.debug(json); 32 | } 33 | if (json.error) { 34 | console.log('\x1b[31m'); 35 | console.log('Error'); 36 | console.log('-----\n'); 37 | console.error(json.error.message); 38 | if (json.error.codeErrors) { 39 | console.log(json.error.codeErrors); 40 | } 41 | console.log('\x1b[0m'); 42 | return; 43 | } 44 | console.log(); 45 | console.log('\x1b[7mResult\x1b[0m'); 46 | console.log('------\n'); 47 | const b = execSync(`echo '${json.evaluationResult}' | jq`, { stdio: 'inherit' }); 48 | console.log(); 49 | console.log('\x1b[7mLogs\x1b[0m'); 50 | console.log('----\n'); 51 | json.logs.forEach((l) => console.log(l)); 52 | console.log(); 53 | // console.log(JSON.stringify(JSON.parse(buffer.toString()).evaluationResult, null, 2)) 54 | } catch (error) { 55 | if (debug && buffer) { 56 | console.debug(buffer.toString()); 57 | } 58 | console.error(error.message); 59 | } 60 | }); 61 | program.parse(); 62 | -------------------------------------------------------------------------------- /scripts/evaluate/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evaluate", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "evaluate", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "commander": "^11.0.0" 13 | } 14 | }, 15 | "node_modules/commander": { 16 | "version": "11.0.0", 17 | "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", 18 | "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", 19 | "engines": { 20 | "node": ">=16" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/evaluate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evaluate", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "evaluate AppSync resolvers and functions", 6 | "main": "index.mjs", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "commander": "^11.0.0" 15 | } 16 | } 17 | --------------------------------------------------------------------------------