├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── API.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── package.json ├── rosetta ├── default.tx.fixture └── with-objects.ts.fixture ├── src ├── index.ts ├── mergeSourceApiSchemaHandler │ └── index.ts ├── private.ts ├── schema-base.ts ├── schema-field.ts ├── schema-intermediate.ts └── source-api-association-merge.ts ├── test ├── appsync-code-first.test.ts ├── appsync-directives.test.ts ├── appsync-enum-type.test.ts ├── appsync-input-types.test.ts ├── appsync-interface-type.test.ts ├── appsync-object-type.test.ts ├── appsync-scalar-type.test.ts ├── appsync-schema.test.ts ├── appsync-union-types.test.ts ├── appsync.test.graphql ├── integ.graphql-schema.ts ├── object-type-definitions.ts ├── scalar-type-defintions.ts └── source-api-association-merge.test.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "@stylistic/indent": [ 48 | "error", 49 | 2 50 | ], 51 | "@stylistic/quotes": [ 52 | "error", 53 | "single", 54 | { 55 | "avoidEscape": true 56 | } 57 | ], 58 | "@stylistic/comma-dangle": [ 59 | "error", 60 | "always-multiline" 61 | ], 62 | "@stylistic/comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "@stylistic/no-multi-spaces": [ 70 | "error", 71 | { 72 | "ignoreEOLComments": false 73 | } 74 | ], 75 | "@stylistic/array-bracket-spacing": [ 76 | "error", 77 | "never" 78 | ], 79 | "@stylistic/array-bracket-newline": [ 80 | "error", 81 | "consistent" 82 | ], 83 | "@stylistic/object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "@stylistic/object-curly-newline": [ 88 | "error", 89 | { 90 | "multiline": true, 91 | "consistent": true 92 | } 93 | ], 94 | "@stylistic/object-property-newline": [ 95 | "error", 96 | { 97 | "allowAllPropertiesOnSameLine": true 98 | } 99 | ], 100 | "@stylistic/keyword-spacing": [ 101 | "error" 102 | ], 103 | "@stylistic/brace-style": [ 104 | "error", 105 | "1tbs", 106 | { 107 | "allowSingleLine": true 108 | } 109 | ], 110 | "@stylistic/space-before-blocks": [ 111 | "error" 112 | ], 113 | "@stylistic/member-delimiter-style": [ 114 | "error" 115 | ], 116 | "@stylistic/semi": [ 117 | "error", 118 | "always" 119 | ], 120 | "@stylistic/max-len": [ 121 | "error", 122 | { 123 | "code": 150, 124 | "ignoreUrls": true, 125 | "ignoreStrings": true, 126 | "ignoreTemplateLiterals": true, 127 | "ignoreComments": true, 128 | "ignoreRegExpLiterals": true 129 | } 130 | ], 131 | "@stylistic/quote-props": [ 132 | "error", 133 | "consistent-as-needed" 134 | ], 135 | "@stylistic/key-spacing": [ 136 | "error" 137 | ], 138 | "@stylistic/no-multiple-empty-lines": [ 139 | "error" 140 | ], 141 | "@stylistic/no-trailing-spaces": [ 142 | "error" 143 | ], 144 | "curly": [ 145 | "error", 146 | "multi-line", 147 | "consistent" 148 | ], 149 | "@typescript-eslint/no-require-imports": "error", 150 | "import/no-extraneous-dependencies": [ 151 | "error", 152 | { 153 | "devDependencies": [ 154 | "**/test/**", 155 | "**/build-tools/**", 156 | ".projenrc.ts", 157 | "projenrc/**/*.ts" 158 | ], 159 | "optionalDependencies": false, 160 | "peerDependencies": true 161 | } 162 | ], 163 | "import/no-unresolved": [ 164 | "error" 165 | ], 166 | "import/order": [ 167 | "warn", 168 | { 169 | "groups": [ 170 | "builtin", 171 | "external" 172 | ], 173 | "alphabetize": { 174 | "order": "asc", 175 | "caseInsensitive": true 176 | } 177 | } 178 | ], 179 | "import/no-duplicates": [ 180 | "error" 181 | ], 182 | "no-shadow": [ 183 | "off" 184 | ], 185 | "@typescript-eslint/no-shadow": "error", 186 | "@typescript-eslint/no-floating-promises": "error", 187 | "no-return-await": [ 188 | "off" 189 | ], 190 | "@typescript-eslint/return-await": "error", 191 | "dot-notation": [ 192 | "error" 193 | ], 194 | "no-bitwise": [ 195 | "error" 196 | ], 197 | "@typescript-eslint/member-ordering": [ 198 | "error", 199 | { 200 | "default": [ 201 | "public-static-field", 202 | "public-static-method", 203 | "protected-static-field", 204 | "protected-static-method", 205 | "private-static-field", 206 | "private-static-method", 207 | "field", 208 | "constructor", 209 | "method" 210 | ] 211 | } 212 | ] 213 | }, 214 | "overrides": [ 215 | { 216 | "files": [ 217 | ".projenrc.ts" 218 | ], 219 | "rules": { 220 | "@typescript-eslint/no-require-imports": "off", 221 | "import/no-extraneous-dependencies": "off" 222 | } 223 | } 224 | ] 225 | } 226 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/build.yml linguist-generated 10 | /.github/workflows/pull-request-lint.yml linguist-generated 11 | /.github/workflows/release.yml linguist-generated 12 | /.github/workflows/upgrade-main.yml linguist-generated 13 | /.gitignore linguist-generated 14 | /.mergify.yml linguist-generated 15 | /.npmignore linguist-generated 16 | /.projen/** linguist-generated 17 | /.projen/deps.json linguist-generated 18 | /.projen/files.json linguist-generated 19 | /.projen/tasks.json linguist-generated 20 | /API.md linguist-generated 21 | /LICENSE linguist-generated 22 | /package.json linguist-generated 23 | /tsconfig.dev.json linguist-generated 24 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'cdklabs-automation') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | working-directory: ./ 36 | - name: Upload patch 37 | if: steps.self_mutation.outputs.self_mutation_happened 38 | uses: actions/upload-artifact@v4.4.0 39 | with: 40 | name: repo.patch 41 | path: repo.patch 42 | overwrite: true 43 | - name: Fail build on mutation 44 | if: steps.self_mutation.outputs.self_mutation_happened 45 | run: |- 46 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 47 | cat repo.patch 48 | exit 1 49 | - name: Backup artifact permissions 50 | run: cd dist && getfacl -R . > permissions-backup.acl 51 | continue-on-error: true 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | self-mutation: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | permissions: 62 | contents: write 63 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 69 | ref: ${{ github.event.pull_request.head.ref }} 70 | repository: ${{ github.event.pull_request.head.repo.full_name }} 71 | - name: Download patch 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: repo.patch 75 | path: ${{ runner.temp }} 76 | - name: Apply patch 77 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 78 | - name: Set git identity 79 | run: |- 80 | git config user.name "github-actions" 81 | git config user.email "github-actions@github.com" 82 | - name: Push changes 83 | env: 84 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 85 | run: |- 86 | git add . 87 | git commit -s -m "chore: self mutation" 88 | git push origin HEAD:$PULL_REQUEST_REF 89 | package-js: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | permissions: 93 | contents: read 94 | if: ${{ !needs.build.outputs.self_mutation_happened }} 95 | steps: 96 | - uses: actions/setup-node@v4 97 | with: 98 | node-version: lts/* 99 | - name: Download build artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: build-artifact 103 | path: dist 104 | - name: Restore build artifact permissions 105 | run: cd dist && setfacl --restore=permissions-backup.acl 106 | continue-on-error: true 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ github.event.pull_request.head.ref }} 111 | repository: ${{ github.event.pull_request.head.repo.full_name }} 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | package-java: 124 | needs: build 125 | runs-on: ubuntu-latest 126 | permissions: 127 | contents: read 128 | if: ${{ !needs.build.outputs.self_mutation_happened }} 129 | steps: 130 | - uses: actions/setup-java@v4 131 | with: 132 | distribution: corretto 133 | java-version: "11" 134 | - uses: actions/setup-node@v4 135 | with: 136 | node-version: lts/* 137 | - name: Download build artifacts 138 | uses: actions/download-artifact@v4 139 | with: 140 | name: build-artifact 141 | path: dist 142 | - name: Restore build artifact permissions 143 | run: cd dist && setfacl --restore=permissions-backup.acl 144 | continue-on-error: true 145 | - name: Checkout 146 | uses: actions/checkout@v4 147 | with: 148 | ref: ${{ github.event.pull_request.head.ref }} 149 | repository: ${{ github.event.pull_request.head.repo.full_name }} 150 | path: .repo 151 | - name: Install Dependencies 152 | run: cd .repo && yarn install --check-files --frozen-lockfile 153 | - name: Extract build artifact 154 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 155 | - name: Move build artifact out of the way 156 | run: mv dist dist.old 157 | - name: Create java artifact 158 | run: cd .repo && npx projen package:java 159 | - name: Collect java artifact 160 | run: mv .repo/dist dist 161 | package-python: 162 | needs: build 163 | runs-on: ubuntu-latest 164 | permissions: 165 | contents: read 166 | if: ${{ !needs.build.outputs.self_mutation_happened }} 167 | steps: 168 | - uses: actions/setup-node@v4 169 | with: 170 | node-version: lts/* 171 | - uses: actions/setup-python@v5 172 | with: 173 | python-version: 3.x 174 | - name: Download build artifacts 175 | uses: actions/download-artifact@v4 176 | with: 177 | name: build-artifact 178 | path: dist 179 | - name: Restore build artifact permissions 180 | run: cd dist && setfacl --restore=permissions-backup.acl 181 | continue-on-error: true 182 | - name: Checkout 183 | uses: actions/checkout@v4 184 | with: 185 | ref: ${{ github.event.pull_request.head.ref }} 186 | repository: ${{ github.event.pull_request.head.repo.full_name }} 187 | path: .repo 188 | - name: Install Dependencies 189 | run: cd .repo && yarn install --check-files --frozen-lockfile 190 | - name: Extract build artifact 191 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 192 | - name: Move build artifact out of the way 193 | run: mv dist dist.old 194 | - name: Create python artifact 195 | run: cd .repo && npx projen package:python 196 | - name: Collect python artifact 197 | run: mv .repo/dist dist 198 | package-dotnet: 199 | needs: build 200 | runs-on: ubuntu-latest 201 | permissions: 202 | contents: read 203 | if: ${{ !needs.build.outputs.self_mutation_happened }} 204 | steps: 205 | - uses: actions/setup-node@v4 206 | with: 207 | node-version: lts/* 208 | - uses: actions/setup-dotnet@v4 209 | with: 210 | dotnet-version: 6.x 211 | - name: Download build artifacts 212 | uses: actions/download-artifact@v4 213 | with: 214 | name: build-artifact 215 | path: dist 216 | - name: Restore build artifact permissions 217 | run: cd dist && setfacl --restore=permissions-backup.acl 218 | continue-on-error: true 219 | - name: Checkout 220 | uses: actions/checkout@v4 221 | with: 222 | ref: ${{ github.event.pull_request.head.ref }} 223 | repository: ${{ github.event.pull_request.head.repo.full_name }} 224 | path: .repo 225 | - name: Install Dependencies 226 | run: cd .repo && yarn install --check-files --frozen-lockfile 227 | - name: Extract build artifact 228 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 229 | - name: Move build artifact out of the way 230 | run: mv dist dist.old 231 | - name: Create dotnet artifact 232 | run: cd .repo && npx projen package:dotnet 233 | - name: Collect dotnet artifact 234 | run: mv .repo/dist dist 235 | package-go: 236 | needs: build 237 | runs-on: ubuntu-latest 238 | permissions: 239 | contents: read 240 | if: ${{ !needs.build.outputs.self_mutation_happened }} 241 | steps: 242 | - uses: actions/setup-node@v4 243 | with: 244 | node-version: lts/* 245 | - uses: actions/setup-go@v5 246 | with: 247 | go-version: ^1.18.0 248 | - name: Download build artifacts 249 | uses: actions/download-artifact@v4 250 | with: 251 | name: build-artifact 252 | path: dist 253 | - name: Restore build artifact permissions 254 | run: cd dist && setfacl --restore=permissions-backup.acl 255 | continue-on-error: true 256 | - name: Checkout 257 | uses: actions/checkout@v4 258 | with: 259 | ref: ${{ github.event.pull_request.head.ref }} 260 | repository: ${{ github.event.pull_request.head.repo.full_name }} 261 | path: .repo 262 | - name: Install Dependencies 263 | run: cd .repo && yarn install --check-files --frozen-lockfile 264 | - name: Extract build artifact 265 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 266 | - name: Move build artifact out of the way 267 | run: mv dist dist.old 268 | - name: Create go artifact 269 | run: cd .repo && npx projen package:go 270 | - name: Collect go artifact 271 | run: mv .repo/dist dist 272 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | - release_maven 67 | - release_pypi 68 | - release_nuget 69 | - release_golang 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: write 73 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 74 | steps: 75 | - uses: actions/setup-node@v4 76 | with: 77 | node-version: lts/* 78 | - name: Download build artifacts 79 | uses: actions/download-artifact@v4 80 | with: 81 | name: build-artifact 82 | path: dist 83 | - name: Restore build artifact permissions 84 | run: cd dist && setfacl --restore=permissions-backup.acl 85 | continue-on-error: true 86 | - name: Release 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_SHA 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 90 | release_npm: 91 | name: Publish to npm 92 | needs: release 93 | runs-on: ubuntu-latest 94 | permissions: 95 | id-token: write 96 | contents: read 97 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 98 | steps: 99 | - uses: actions/setup-node@v4 100 | with: 101 | node-version: lts/* 102 | - name: Download build artifacts 103 | uses: actions/download-artifact@v4 104 | with: 105 | name: build-artifact 106 | path: dist 107 | - name: Restore build artifact permissions 108 | run: cd dist && setfacl --restore=permissions-backup.acl 109 | continue-on-error: true 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | with: 113 | path: .repo 114 | - name: Install Dependencies 115 | run: cd .repo && yarn install --check-files --frozen-lockfile 116 | - name: Extract build artifact 117 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 118 | - name: Move build artifact out of the way 119 | run: mv dist dist.old 120 | - name: Create js artifact 121 | run: cd .repo && npx projen package:js 122 | - name: Collect js artifact 123 | run: mv .repo/dist dist 124 | - name: Release 125 | env: 126 | NPM_DIST_TAG: latest 127 | NPM_REGISTRY: registry.npmjs.org 128 | NPM_CONFIG_PROVENANCE: "true" 129 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 130 | run: npx -p publib@latest publib-npm 131 | release_maven: 132 | name: Publish to Maven Central 133 | needs: release 134 | runs-on: ubuntu-latest 135 | permissions: 136 | contents: read 137 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 138 | steps: 139 | - uses: actions/setup-java@v4 140 | with: 141 | distribution: corretto 142 | java-version: "11" 143 | - uses: actions/setup-node@v4 144 | with: 145 | node-version: lts/* 146 | - name: Download build artifacts 147 | uses: actions/download-artifact@v4 148 | with: 149 | name: build-artifact 150 | path: dist 151 | - name: Restore build artifact permissions 152 | run: cd dist && setfacl --restore=permissions-backup.acl 153 | continue-on-error: true 154 | - name: Checkout 155 | uses: actions/checkout@v4 156 | with: 157 | path: .repo 158 | - name: Install Dependencies 159 | run: cd .repo && yarn install --check-files --frozen-lockfile 160 | - name: Extract build artifact 161 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 162 | - name: Move build artifact out of the way 163 | run: mv dist dist.old 164 | - name: Create java artifact 165 | run: cd .repo && npx projen package:java 166 | - name: Collect java artifact 167 | run: mv .repo/dist dist 168 | - name: Release 169 | env: 170 | MAVEN_SERVER_ID: central-ossrh 171 | MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 172 | MAVEN_GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE }} 173 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 174 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 175 | MAVEN_STAGING_PROFILE_ID: ${{ secrets.MAVEN_STAGING_PROFILE_ID }} 176 | run: npx -p publib@latest publib-maven 177 | release_pypi: 178 | name: Publish to PyPI 179 | needs: release 180 | runs-on: ubuntu-latest 181 | permissions: 182 | contents: read 183 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 184 | steps: 185 | - uses: actions/setup-node@v4 186 | with: 187 | node-version: lts/* 188 | - uses: actions/setup-python@v5 189 | with: 190 | python-version: 3.x 191 | - name: Download build artifacts 192 | uses: actions/download-artifact@v4 193 | with: 194 | name: build-artifact 195 | path: dist 196 | - name: Restore build artifact permissions 197 | run: cd dist && setfacl --restore=permissions-backup.acl 198 | continue-on-error: true 199 | - name: Checkout 200 | uses: actions/checkout@v4 201 | with: 202 | path: .repo 203 | - name: Install Dependencies 204 | run: cd .repo && yarn install --check-files --frozen-lockfile 205 | - name: Extract build artifact 206 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 207 | - name: Move build artifact out of the way 208 | run: mv dist dist.old 209 | - name: Create python artifact 210 | run: cd .repo && npx projen package:python 211 | - name: Collect python artifact 212 | run: mv .repo/dist dist 213 | - name: Release 214 | env: 215 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 216 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 217 | run: npx -p publib@latest publib-pypi 218 | release_nuget: 219 | name: Publish to NuGet Gallery 220 | needs: release 221 | runs-on: ubuntu-latest 222 | permissions: 223 | contents: read 224 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 225 | steps: 226 | - uses: actions/setup-node@v4 227 | with: 228 | node-version: lts/* 229 | - uses: actions/setup-dotnet@v4 230 | with: 231 | dotnet-version: 6.x 232 | - name: Download build artifacts 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: build-artifact 236 | path: dist 237 | - name: Restore build artifact permissions 238 | run: cd dist && setfacl --restore=permissions-backup.acl 239 | continue-on-error: true 240 | - name: Checkout 241 | uses: actions/checkout@v4 242 | with: 243 | path: .repo 244 | - name: Install Dependencies 245 | run: cd .repo && yarn install --check-files --frozen-lockfile 246 | - name: Extract build artifact 247 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 248 | - name: Move build artifact out of the way 249 | run: mv dist dist.old 250 | - name: Create dotnet artifact 251 | run: cd .repo && npx projen package:dotnet 252 | - name: Collect dotnet artifact 253 | run: mv .repo/dist dist 254 | - name: Release 255 | env: 256 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 257 | run: npx -p publib@latest publib-nuget 258 | release_golang: 259 | name: Publish to GitHub Go Module Repository 260 | needs: release 261 | runs-on: ubuntu-latest 262 | permissions: 263 | contents: read 264 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 265 | steps: 266 | - uses: actions/setup-node@v4 267 | with: 268 | node-version: lts/* 269 | - uses: actions/setup-go@v5 270 | with: 271 | go-version: ^1.18.0 272 | - name: Download build artifacts 273 | uses: actions/download-artifact@v4 274 | with: 275 | name: build-artifact 276 | path: dist 277 | - name: Restore build artifact permissions 278 | run: cd dist && setfacl --restore=permissions-backup.acl 279 | continue-on-error: true 280 | - name: Checkout 281 | uses: actions/checkout@v4 282 | with: 283 | path: .repo 284 | - name: Install Dependencies 285 | run: cd .repo && yarn install --check-files --frozen-lockfile 286 | - name: Extract build artifact 287 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 288 | - name: Move build artifact out of the way 289 | run: mv dist dist.old 290 | - name: Create go artifact 291 | run: cd .repo && npx projen package:go 292 | - name: Collect go artifact 293 | run: mv .repo/dist dist 294 | - name: Release 295 | env: 296 | GIT_USER_NAME: github-actions 297 | GIT_USER_EMAIL: github-actions@github.com 298 | GITHUB_TOKEN: ${{ secrets.GO_GITHUB_TOKEN }} 299 | run: npx -p publib@latest publib-golang 300 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "chore(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.mergify.yml 42 | !/.github/workflows/upgrade-main.yml 43 | !/.github/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.dev.json 46 | !/src/ 47 | /lib 48 | /dist/ 49 | !/.eslintrc.json 50 | .jsii 51 | tsconfig.json 52 | !/API.md 53 | !/.projenrc.ts 54 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | - status-success=package-java 12 | - status-success=package-python 13 | - status-success=package-dotnet 14 | - status-success=package-go 15 | merge_method: squash 16 | commit_message_template: |- 17 | {{ title }} (#{{ number }}) 18 | 19 | {{ body }} 20 | pull_request_rules: 21 | - name: Automatic merge on approval and successful build 22 | actions: 23 | delete_head_branch: {} 24 | queue: 25 | name: default 26 | conditions: 27 | - "#approved-reviews-by>=1" 28 | - -label~=(do-not-merge) 29 | - status-success=build 30 | - status-success=package-js 31 | - status-success=package-java 32 | - status-success=package-python 33 | - status-success=package-dotnet 34 | - status-success=package-go 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | /.gitattributes 26 | /.projenrc.ts 27 | /projenrc 28 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-sdk/client-appsync", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@stylistic/eslint-plugin", 9 | "version": "^2", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@types/aws-lambda", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@types/jest", 18 | "type": "build" 19 | }, 20 | { 21 | "name": "@types/node", 22 | "type": "build" 23 | }, 24 | { 25 | "name": "@typescript-eslint/eslint-plugin", 26 | "version": "^8", 27 | "type": "build" 28 | }, 29 | { 30 | "name": "@typescript-eslint/parser", 31 | "version": "^8", 32 | "type": "build" 33 | }, 34 | { 35 | "name": "commit-and-tag-version", 36 | "version": "^12", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint-import-resolver-typescript", 41 | "type": "build" 42 | }, 43 | { 44 | "name": "eslint-plugin-import", 45 | "type": "build" 46 | }, 47 | { 48 | "name": "eslint", 49 | "version": "^9", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "jest", 54 | "type": "build" 55 | }, 56 | { 57 | "name": "jest-junit", 58 | "version": "^16", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "jsii-diff", 63 | "type": "build" 64 | }, 65 | { 66 | "name": "jsii-docgen", 67 | "version": "^10.5.0", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "jsii-pacmak", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "jsii-rosetta", 76 | "version": "~5.6.0", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "jsii", 81 | "version": "~5.6.0", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "projen", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "ts-jest", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "ts-node", 94 | "type": "build" 95 | }, 96 | { 97 | "name": "typescript", 98 | "type": "build" 99 | }, 100 | { 101 | "name": "aws-cdk-lib", 102 | "version": "^2.110.0", 103 | "type": "peer" 104 | }, 105 | { 106 | "name": "constructs", 107 | "version": "^10.0.5", 108 | "type": "peer" 109 | } 110 | ], 111 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 112 | } 113 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-main.yml", 11 | ".gitignore", 12 | ".mergify.yml", 13 | ".projen/deps.json", 14 | ".projen/files.json", 15 | ".projen/tasks.json", 16 | "LICENSE", 17 | "tsconfig.dev.json" 18 | ], 19 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 20 | } 21 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 37 | }, 38 | "steps": [ 39 | { 40 | "builtin": "release/bump-version" 41 | } 42 | ], 43 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 44 | }, 45 | "clobber": { 46 | "name": "clobber", 47 | "description": "hard resets to HEAD of origin and cleans the local repo", 48 | "env": { 49 | "BRANCH": "$(git branch --show-current)" 50 | }, 51 | "steps": [ 52 | { 53 | "exec": "git checkout -b scratch", 54 | "name": "save current HEAD in \"scratch\" branch" 55 | }, 56 | { 57 | "exec": "git checkout $BRANCH" 58 | }, 59 | { 60 | "exec": "git fetch origin", 61 | "name": "fetch latest changes from origin" 62 | }, 63 | { 64 | "exec": "git reset --hard origin/$BRANCH", 65 | "name": "hard reset to origin commit" 66 | }, 67 | { 68 | "exec": "git clean -fdx", 69 | "name": "clean all untracked files" 70 | }, 71 | { 72 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 73 | } 74 | ], 75 | "condition": "git diff --exit-code > /dev/null" 76 | }, 77 | "compat": { 78 | "name": "compat", 79 | "description": "Perform API compatibility check against latest version", 80 | "steps": [ 81 | { 82 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 83 | } 84 | ] 85 | }, 86 | "compile": { 87 | "name": "compile", 88 | "description": "Only compile", 89 | "steps": [ 90 | { 91 | "exec": "jsii --silence-warnings=reserved-word" 92 | } 93 | ] 94 | }, 95 | "default": { 96 | "name": "default", 97 | "description": "Synthesize project files", 98 | "steps": [ 99 | { 100 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 101 | } 102 | ] 103 | }, 104 | "docgen": { 105 | "name": "docgen", 106 | "description": "Generate API.md from .jsii manifest", 107 | "steps": [ 108 | { 109 | "exec": "jsii-docgen -o API.md" 110 | } 111 | ] 112 | }, 113 | "eject": { 114 | "name": "eject", 115 | "description": "Remove projen from the project", 116 | "env": { 117 | "PROJEN_EJECTING": "true" 118 | }, 119 | "steps": [ 120 | { 121 | "spawn": "default" 122 | } 123 | ] 124 | }, 125 | "eslint": { 126 | "name": "eslint", 127 | "description": "Runs eslint against the codebase", 128 | "env": { 129 | "ESLINT_USE_FLAT_CONFIG": "false" 130 | }, 131 | "steps": [ 132 | { 133 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 134 | "receiveArgs": true 135 | } 136 | ] 137 | }, 138 | "install": { 139 | "name": "install", 140 | "description": "Install project dependencies and update lockfile (non-frozen)", 141 | "steps": [ 142 | { 143 | "exec": "yarn install --check-files" 144 | } 145 | ] 146 | }, 147 | "install:ci": { 148 | "name": "install:ci", 149 | "description": "Install project dependencies using frozen lockfile", 150 | "steps": [ 151 | { 152 | "exec": "yarn install --check-files --frozen-lockfile" 153 | } 154 | ] 155 | }, 156 | "package": { 157 | "name": "package", 158 | "description": "Creates the distribution package", 159 | "steps": [ 160 | { 161 | "spawn": "package:js", 162 | "condition": "node -e \"if (!process.env.CI) process.exit(1)\"" 163 | }, 164 | { 165 | "spawn": "package-all", 166 | "condition": "node -e \"if (process.env.CI) process.exit(1)\"" 167 | } 168 | ] 169 | }, 170 | "package-all": { 171 | "name": "package-all", 172 | "description": "Packages artifacts for all target languages", 173 | "steps": [ 174 | { 175 | "spawn": "package:js" 176 | }, 177 | { 178 | "spawn": "package:java" 179 | }, 180 | { 181 | "spawn": "package:python" 182 | }, 183 | { 184 | "spawn": "package:dotnet" 185 | }, 186 | { 187 | "spawn": "package:go" 188 | } 189 | ] 190 | }, 191 | "package:dotnet": { 192 | "name": "package:dotnet", 193 | "description": "Create dotnet language bindings", 194 | "steps": [ 195 | { 196 | "exec": "jsii-pacmak -v --target dotnet" 197 | } 198 | ] 199 | }, 200 | "package:go": { 201 | "name": "package:go", 202 | "description": "Create go language bindings", 203 | "steps": [ 204 | { 205 | "exec": "jsii-pacmak -v --target go" 206 | } 207 | ] 208 | }, 209 | "package:java": { 210 | "name": "package:java", 211 | "description": "Create java language bindings", 212 | "steps": [ 213 | { 214 | "exec": "jsii-pacmak -v --target java" 215 | } 216 | ] 217 | }, 218 | "package:js": { 219 | "name": "package:js", 220 | "description": "Create js language bindings", 221 | "steps": [ 222 | { 223 | "exec": "jsii-pacmak -v --target js" 224 | } 225 | ] 226 | }, 227 | "package:python": { 228 | "name": "package:python", 229 | "description": "Create python language bindings", 230 | "steps": [ 231 | { 232 | "exec": "jsii-pacmak -v --target python" 233 | } 234 | ] 235 | }, 236 | "post-compile": { 237 | "name": "post-compile", 238 | "description": "Runs after successful compilation", 239 | "steps": [ 240 | { 241 | "spawn": "docgen" 242 | } 243 | ] 244 | }, 245 | "post-upgrade": { 246 | "name": "post-upgrade", 247 | "description": "Runs after upgrading dependencies" 248 | }, 249 | "pre-compile": { 250 | "name": "pre-compile", 251 | "description": "Prepare the project for compilation" 252 | }, 253 | "release": { 254 | "name": "release", 255 | "description": "Prepare a release from \"main\" branch", 256 | "env": { 257 | "RELEASE": "true" 258 | }, 259 | "steps": [ 260 | { 261 | "exec": "rm -fr dist" 262 | }, 263 | { 264 | "spawn": "bump" 265 | }, 266 | { 267 | "spawn": "build" 268 | }, 269 | { 270 | "spawn": "unbump" 271 | }, 272 | { 273 | "exec": "git diff --ignore-space-at-eol --exit-code" 274 | } 275 | ] 276 | }, 277 | "test": { 278 | "name": "test", 279 | "description": "Run tests", 280 | "steps": [ 281 | { 282 | "exec": "jest --passWithNoTests --updateSnapshot", 283 | "receiveArgs": true 284 | }, 285 | { 286 | "spawn": "eslint" 287 | } 288 | ] 289 | }, 290 | "test:watch": { 291 | "name": "test:watch", 292 | "description": "Run jest in watch mode", 293 | "steps": [ 294 | { 295 | "exec": "jest --watch" 296 | } 297 | ] 298 | }, 299 | "unbump": { 300 | "name": "unbump", 301 | "description": "Restores version to 0.0.0", 302 | "env": { 303 | "OUTFILE": "package.json", 304 | "CHANGELOG": "dist/changelog.md", 305 | "BUMPFILE": "dist/version.txt", 306 | "RELEASETAG": "dist/releasetag.txt", 307 | "RELEASE_TAG_PREFIX": "", 308 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 309 | }, 310 | "steps": [ 311 | { 312 | "builtin": "release/reset-version" 313 | } 314 | ] 315 | }, 316 | "upgrade": { 317 | "name": "upgrade", 318 | "description": "upgrade dependencies", 319 | "env": { 320 | "CI": "0" 321 | }, 322 | "steps": [ 323 | { 324 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@aws-sdk/client-appsync,@types/aws-lambda,@types/jest,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,jsii-diff,jsii-pacmak,projen,ts-jest,ts-node,typescript" 325 | }, 326 | { 327 | "exec": "yarn install --check-files" 328 | }, 329 | { 330 | "exec": "yarn upgrade @aws-sdk/client-appsync @stylistic/eslint-plugin @types/aws-lambda @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen ts-jest ts-node typescript aws-cdk-lib constructs" 331 | }, 332 | { 333 | "exec": "npx projen" 334 | }, 335 | { 336 | "spawn": "post-upgrade" 337 | } 338 | ] 339 | }, 340 | "watch": { 341 | "name": "watch", 342 | "description": "Watch & compile in the background", 343 | "steps": [ 344 | { 345 | "exec": "jsii -w --silence-warnings=reserved-word" 346 | } 347 | ] 348 | } 349 | }, 350 | "env": { 351 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 352 | }, 353 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 354 | } 355 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { awscdk } from 'projen'; 2 | 3 | const project = new awscdk.AwsCdkConstructLibrary({ 4 | author: 'Mitchell Valine', 5 | authorAddress: 'mitchellvaline@yahoo.com', 6 | cdkVersion: '2.110.0', 7 | defaultReleaseBranch: 'main', 8 | name: 'awscdk-appsync-utils', 9 | projenrcTs: true, 10 | repositoryUrl: 'https://github.com/cdklabs/awscdk-appsync-utils.git', 11 | description: 'Utilities for creating appsync apis using aws-cdk', 12 | // Auto approve PRs by our bot 13 | autoApproveOptions: { 14 | allowedUsernames: ['cdklabs-automation'], 15 | secret: 'GITHUB_TOKEN', 16 | }, 17 | autoApproveUpgrades: true, 18 | publishToMaven: { 19 | javaPackage: 'io.github.cdklabs.awscdk.appsync.utils', 20 | mavenGroupId: 'io.github.cdklabs', 21 | mavenArtifactId: 'awscdk-appsync-utils', 22 | mavenServerId: 'central-ossrh', 23 | }, 24 | publishToNuget: { 25 | dotNetNamespace: 'Cdklabs.AwsCdkAppsyncUtils', 26 | packageId: 'Cdklabs.AwsCdkAppsyncUtils', 27 | }, 28 | publishToPypi: { 29 | distName: 'cdklabs.appsync-utils', 30 | module: 'awscdk.appsync_utils', 31 | }, 32 | publishToGo: { 33 | moduleName: 'github.com/cdklabs/awscdk-appsync-utils-go', 34 | }, 35 | }); 36 | 37 | project.addDevDeps('@aws-sdk/client-appsync'); 38 | project.addDevDeps('@types/aws-lambda'); 39 | project.synth(); 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awscdk-appsync-utils", 3 | "description": "Utilities for creating appsync apis using aws-cdk", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cdklabs/awscdk-appsync-utils.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compat": "npx projen compat", 13 | "compile": "npx projen compile", 14 | "default": "npx projen default", 15 | "docgen": "npx projen docgen", 16 | "eject": "npx projen eject", 17 | "eslint": "npx projen eslint", 18 | "package": "npx projen package", 19 | "package-all": "npx projen package-all", 20 | "package:dotnet": "npx projen package:dotnet", 21 | "package:go": "npx projen package:go", 22 | "package:java": "npx projen package:java", 23 | "package:js": "npx projen package:js", 24 | "package:python": "npx projen package:python", 25 | "post-compile": "npx projen post-compile", 26 | "post-upgrade": "npx projen post-upgrade", 27 | "pre-compile": "npx projen pre-compile", 28 | "release": "npx projen release", 29 | "test": "npx projen test", 30 | "test:watch": "npx projen test:watch", 31 | "unbump": "npx projen unbump", 32 | "upgrade": "npx projen upgrade", 33 | "watch": "npx projen watch", 34 | "projen": "npx projen" 35 | }, 36 | "author": { 37 | "name": "Mitchell Valine", 38 | "email": "mitchellvaline@yahoo.com", 39 | "organization": false 40 | }, 41 | "devDependencies": { 42 | "@aws-sdk/client-appsync": "^3.821.0", 43 | "@stylistic/eslint-plugin": "^2", 44 | "@types/aws-lambda": "^8.10.149", 45 | "@types/jest": "^27", 46 | "@types/node": "^16 <= 16.18.78", 47 | "@typescript-eslint/eslint-plugin": "^8", 48 | "@typescript-eslint/parser": "^8", 49 | "aws-cdk-lib": "2.110.0", 50 | "commit-and-tag-version": "^12", 51 | "constructs": "10.0.5", 52 | "eslint": "^9", 53 | "eslint-import-resolver-typescript": "^3.10.1", 54 | "eslint-plugin-import": "^2.31.0", 55 | "jest": "^27", 56 | "jest-junit": "^16", 57 | "jsii": "~5.6.0", 58 | "jsii-diff": "^1.112.0", 59 | "jsii-docgen": "^10.5.0", 60 | "jsii-pacmak": "^1.112.0", 61 | "jsii-rosetta": "~5.6.0", 62 | "projen": "^0.92.9", 63 | "ts-jest": "^27", 64 | "ts-node": "^10.9.2", 65 | "typescript": "^4.9.5" 66 | }, 67 | "peerDependencies": { 68 | "aws-cdk-lib": "^2.110.0", 69 | "constructs": "^10.0.5" 70 | }, 71 | "keywords": [ 72 | "cdk" 73 | ], 74 | "main": "lib/index.js", 75 | "license": "Apache-2.0", 76 | "publishConfig": { 77 | "access": "public" 78 | }, 79 | "version": "0.0.0", 80 | "jest": { 81 | "coverageProvider": "v8", 82 | "testMatch": [ 83 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 84 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 85 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 86 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 87 | ], 88 | "clearMocks": true, 89 | "collectCoverage": true, 90 | "coverageReporters": [ 91 | "json", 92 | "lcov", 93 | "clover", 94 | "cobertura", 95 | "text" 96 | ], 97 | "coverageDirectory": "coverage", 98 | "coveragePathIgnorePatterns": [ 99 | "/node_modules/" 100 | ], 101 | "testPathIgnorePatterns": [ 102 | "/node_modules/" 103 | ], 104 | "watchPathIgnorePatterns": [ 105 | "/node_modules/" 106 | ], 107 | "reporters": [ 108 | "default", 109 | [ 110 | "jest-junit", 111 | { 112 | "outputDirectory": "test-reports" 113 | } 114 | ] 115 | ], 116 | "preset": "ts-jest", 117 | "globals": { 118 | "ts-jest": { 119 | "tsconfig": "tsconfig.dev.json" 120 | } 121 | } 122 | }, 123 | "types": "lib/index.d.ts", 124 | "stability": "stable", 125 | "jsii": { 126 | "outdir": "dist", 127 | "targets": { 128 | "java": { 129 | "package": "io.github.cdklabs.awscdk.appsync.utils", 130 | "maven": { 131 | "groupId": "io.github.cdklabs", 132 | "artifactId": "awscdk-appsync-utils" 133 | } 134 | }, 135 | "python": { 136 | "distName": "cdklabs.appsync-utils", 137 | "module": "awscdk.appsync_utils" 138 | }, 139 | "dotnet": { 140 | "namespace": "Cdklabs.AwsCdkAppsyncUtils", 141 | "packageId": "Cdklabs.AwsCdkAppsyncUtils" 142 | }, 143 | "go": { 144 | "moduleName": "github.com/cdklabs/awscdk-appsync-utils-go" 145 | } 146 | }, 147 | "tsc": { 148 | "outDir": "lib", 149 | "rootDir": "src" 150 | } 151 | }, 152 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 153 | } 154 | -------------------------------------------------------------------------------- /rosetta/default.tx.fixture: -------------------------------------------------------------------------------- 1 | // Fixture with packages imported, but nothing else 2 | import { RemovalPolicy, Stack } from '@aws-cdk/core'; 3 | import { Construct } from 'constructs'; 4 | import * as appsync from '@aws-cdk/aws-appsync-alpha'; 5 | import ec2 = require('@aws-cdk/aws-ec2'); 6 | import dynamodb = require('@aws-cdk/aws-dynamodb'); 7 | import iam = require('@aws-cdk/aws-iam'); 8 | import rds = require('@aws-cdk/aws-rds'); 9 | import path = require('path'); 10 | import { GraphqlType, InterfaceType, EnumType, InputType, UnionType, Field, ObjectType, ResolvableField, CodeFirstSchema } from 'awscdk-appsync-utils'; 11 | 12 | class Fixture extends Stack { 13 | constructor(scope: Construct, id: string) { 14 | super(scope, id); 15 | /// here 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rosetta/with-objects.ts.fixture: -------------------------------------------------------------------------------- 1 | // Fixture with packages imported, but nothing else 2 | import { Stack } from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | import * as appsync from '@aws-cdk/aws-appsync-alpha'; 5 | import { CodeFirstSchema, GraphqlType, InterfaceType, ObjectType, ResolvableField } from 'awscdk-appsync-utils'; 6 | 7 | const args = { 8 | after: GraphqlType.string(), 9 | first: GraphqlType.int(), 10 | before: GraphqlType.string(), 11 | last: GraphqlType.int(), 12 | }; 13 | 14 | const Node = new InterfaceType('Node', { 15 | definition: { id: GraphqlType.string() } 16 | }); 17 | 18 | const FilmNode = new ObjectType('FilmNode', { 19 | interfaceTypes: [Node], 20 | definition: { filmName: GraphqlType.string() } 21 | }); 22 | 23 | function generateEdgeAndConnection(base: ObjectType) { 24 | const edge = new ObjectType(`${base.name}Edge`, { 25 | definition: { node: base.attribute(), cursor: GraphqlType.string() } 26 | }); 27 | const connection = new ObjectType(`${base.name}Connection`, { 28 | definition: { 29 | edges: edge.attribute({ isList: true }), 30 | [pluralize(base.name)]: base.attribute({ isList: true }), 31 | totalCount: GraphqlType.int(), 32 | } 33 | }); 34 | return { edge: edge, connection: connection }; 35 | } 36 | 37 | const demo = new ObjectType('Demo', { 38 | definition: { 39 | id: GraphqlType.string({ isRequired: true }), 40 | version: GraphqlType.string({ isRequired: true }), 41 | }, 42 | }); 43 | 44 | class Fixture extends Stack { 45 | constructor(scope: Construct, id: string) { 46 | super(scope, id); 47 | 48 | /// here 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Lazy } from 'aws-cdk-lib'; 2 | import { SchemaBindOptions, ISchema, ISchemaConfig, IGraphqlApi } from 'aws-cdk-lib/aws-appsync'; 3 | import { shapeAddition } from './private'; 4 | import { IIntermediateType } from './schema-base'; 5 | import { Field, ResolvableField } from './schema-field'; 6 | import { ObjectType } from './schema-intermediate'; 7 | export * from './schema-base'; 8 | export * from './schema-field'; 9 | export * from './schema-intermediate'; 10 | export * from './source-api-association-merge'; 11 | 12 | export class CodeFirstSchema implements ISchema { 13 | /** 14 | * The definition for this schema 15 | */ 16 | public definition: string; 17 | 18 | private query?: ObjectType; 19 | 20 | private mutation?: ObjectType; 21 | 22 | private subscription?: ObjectType; 23 | 24 | private types: IIntermediateType[]; 25 | 26 | public constructor() { 27 | this.definition = ''; 28 | this.types = []; 29 | } 30 | 31 | /** 32 | * Called when the GraphQL Api is initialized to allow this object to bind 33 | * to the stack. 34 | * 35 | * @param api The binding GraphQL Api 36 | */ 37 | public bind(api: IGraphqlApi, _options?: SchemaBindOptions): ISchemaConfig { 38 | return { 39 | apiId: api.apiId, 40 | definition: Lazy.string({ 41 | produce: () => this.types.reduce((acc, type) => `${acc}${type._bindToGraphqlApi(api).toString()}\n`, 42 | `${this.declareSchema()}${this.definition}`), 43 | }), 44 | }; 45 | } 46 | 47 | /** 48 | * Escape hatch to add to Schema as desired. Will always result 49 | * in a newline. 50 | * 51 | * @param addition the addition to add to schema 52 | * @param delimiter the delimiter between schema and addition 53 | * @default - '' 54 | * 55 | */ 56 | public addToSchema(addition: string, delimiter?: string): void { 57 | const sep = delimiter ?? ''; 58 | this.definition = `${this.definition}${sep}${addition}\n`; 59 | } 60 | 61 | /** 62 | * Add a query field to the schema's Query. CDK will create an 63 | * Object Type called 'Query'. For example, 64 | * 65 | * type Query { 66 | * fieldName: Field.returnType 67 | * } 68 | * 69 | * @param fieldName the name of the query 70 | * @param field the resolvable field to for this query 71 | */ 72 | public addQuery(fieldName: string, field: ResolvableField): ObjectType { 73 | if (!this.query) { 74 | this.query = new ObjectType('Query', { definition: {} }); 75 | this.addType(this.query); 76 | }; 77 | this.query.addField({ fieldName, field }); 78 | return this.query; 79 | } 80 | 81 | /** 82 | * Add a mutation field to the schema's Mutation. CDK will create an 83 | * Object Type called 'Mutation'. For example, 84 | * 85 | * type Mutation { 86 | * fieldName: Field.returnType 87 | * } 88 | * 89 | * @param fieldName the name of the Mutation 90 | * @param field the resolvable field to for this Mutation 91 | */ 92 | public addMutation(fieldName: string, field: ResolvableField): ObjectType { 93 | if (!this.mutation) { 94 | this.mutation = new ObjectType('Mutation', { definition: {} }); 95 | this.addType(this.mutation); 96 | }; 97 | this.mutation.addField({ fieldName, field }); 98 | return this.mutation; 99 | } 100 | 101 | /** 102 | * Add a subscription field to the schema's Subscription. CDK will create an 103 | * Object Type called 'Subscription'. For example, 104 | * 105 | * type Subscription { 106 | * fieldName: Field.returnType 107 | * } 108 | * 109 | * @param fieldName the name of the Subscription 110 | * @param field the resolvable field to for this Subscription 111 | */ 112 | public addSubscription(fieldName: string, field: Field): ObjectType { 113 | if (!this.subscription) { 114 | this.subscription = new ObjectType('Subscription', { definition: {} }); 115 | this.addType(this.subscription); 116 | } 117 | const directives = field.fieldOptions?.directives?.filter((directive) => directive.mutationFields); 118 | if (directives && directives.length > 1) { 119 | throw new Error(`Subscription fields must not have more than one @aws_subscribe directives. Received: ${directives.length}`); 120 | } 121 | this.subscription.addField({ fieldName, field }); 122 | return this.subscription; 123 | } 124 | 125 | /** 126 | * Add type to the schema 127 | * 128 | * @param type the intermediate type to add to the schema 129 | * 130 | */ 131 | public addType(type: IIntermediateType): IIntermediateType { 132 | this.types.push(type); 133 | return type; 134 | } 135 | 136 | /** 137 | * Set the root types of this schema if they are defined. 138 | * 139 | * For example: 140 | * schema { 141 | * query: Query 142 | * mutation: Mutation 143 | * subscription: Subscription 144 | * } 145 | */ 146 | private declareSchema(): string { 147 | if (!this.query && !this.mutation && !this.subscription) { 148 | return ''; 149 | } 150 | type root = 'mutation' | 'query' | 'subscription'; 151 | const list: root[] = ['query', 'mutation', 'subscription']; 152 | return shapeAddition({ 153 | prefix: 'schema', 154 | fields: list.map((key: root) => this[key] ? `${key}: ${this[key]?.name}` : '') 155 | .filter((field) => field != ''), 156 | }) + '\n'; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/mergeSourceApiSchemaHandler/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { 3 | StartSchemaMergeCommand, 4 | AppSyncClient, 5 | SourceApiAssociationStatus, 6 | GetSourceApiAssociationCommand, 7 | GetSourceApiAssociationCommandInput, 8 | } from '@aws-sdk/client-appsync'; 9 | 10 | const appSyncClient = new AppSyncClient(); 11 | 12 | type SchemaMergeResult = { 13 | associationId?: string; 14 | mergedApiIdentifier?: string; 15 | PhysicalResourceId: string; 16 | sourceApiAssociationStatus?: SourceApiAssociationStatus; 17 | }; 18 | 19 | type IsCompleteResult = { 20 | IsComplete: boolean; 21 | Data?: Object; 22 | }; 23 | 24 | export async function onEvent(event: AWSLambda.CloudFormationCustomResourceEvent): Promise { 25 | const params = { 26 | associationId: event.ResourceProperties.associationId, 27 | mergedApiIdentifier: event.ResourceProperties.mergedApiIdentifier, 28 | }; 29 | 30 | switch (event.RequestType) { 31 | case 'Update': 32 | case 'Create': 33 | return performSchemaMerge(params); 34 | case 'Delete': 35 | default: 36 | return { 37 | ...params, 38 | PhysicalResourceId: params.associationId, 39 | }; 40 | } 41 | } 42 | 43 | export async function isComplete(event: AWSLambda.CloudFormationCustomResourceEvent): Promise { 44 | const params = { 45 | associationId: event.ResourceProperties.associationId, 46 | mergedApiIdentifier: event.ResourceProperties.mergedApiIdentifier, 47 | }; 48 | 49 | return getSchemaMergeStatus(params); 50 | } 51 | 52 | async function performSchemaMerge(params: any): Promise { 53 | const command = new StartSchemaMergeCommand(params); 54 | 55 | try { 56 | const response = await appSyncClient.send(command); 57 | switch (response.sourceApiAssociationStatus) { 58 | case SourceApiAssociationStatus.MERGE_SCHEDULED: 59 | case SourceApiAssociationStatus.MERGE_IN_PROGRESS: 60 | case SourceApiAssociationStatus.MERGE_SUCCESS: 61 | break; 62 | default: 63 | throw new Error('Unexpected status after starting schema merge:' + response.sourceApiAssociationStatus); 64 | } 65 | 66 | return { 67 | ...params, 68 | PhysicalResourceId: params.associationId, 69 | sourceApiAssociationStatus: response.sourceApiAssociationStatus, 70 | }; 71 | } catch (error) { 72 | 73 | // eslint-disable-next-line no-console 74 | console.error('An error occurred submitting the schema merge', error); 75 | throw error; 76 | } 77 | } 78 | 79 | async function getSchemaMergeStatus(params: GetSourceApiAssociationCommandInput): Promise { 80 | const command = new GetSourceApiAssociationCommand(params); 81 | var response; 82 | 83 | try { 84 | response = await appSyncClient.send(command); 85 | } catch (error) { 86 | // eslint-disable-next-line no-console 87 | console.error('Error starting the schema merge operation', error); 88 | throw error; 89 | } 90 | 91 | if (!response.sourceApiAssociation) { 92 | throw new Error(`SourceApiAssociation ${params.associationId} not found.`); 93 | } 94 | 95 | switch (response.sourceApiAssociation.sourceApiAssociationStatus) { 96 | case SourceApiAssociationStatus.MERGE_SCHEDULED: 97 | case SourceApiAssociationStatus.MERGE_IN_PROGRESS: 98 | case SourceApiAssociationStatus.DELETION_SCHEDULED: 99 | case SourceApiAssociationStatus.DELETION_IN_PROGRESS: 100 | return { 101 | IsComplete: false, 102 | }; 103 | 104 | case SourceApiAssociationStatus.MERGE_SUCCESS: 105 | return { 106 | IsComplete: true, 107 | Data: { 108 | ...params, 109 | sourceApiAssociationStatus: response.sourceApiAssociation.sourceApiAssociationStatus, 110 | sourceApiAssociationStatusDetail: response.sourceApiAssociation.sourceApiAssociationStatusDetail, 111 | lastSuccessfulMergeDate: response.sourceApiAssociation.lastSuccessfulMergeDate?.toString(), 112 | }, 113 | }; 114 | 115 | case SourceApiAssociationStatus.MERGE_FAILED: 116 | case SourceApiAssociationStatus.DELETION_FAILED: 117 | case SourceApiAssociationStatus.AUTO_MERGE_SCHEDULE_FAILED: 118 | throw new Error(`Source API Association: ${response.sourceApiAssociation.associationArn} failed to merge with status: ` 119 | + `${response.sourceApiAssociation.sourceApiAssociationStatus} and message: ${response.sourceApiAssociation.sourceApiAssociationStatusDetail}`); 120 | 121 | default: 122 | throw new Error(`Unexpected source api association status: ${response.sourceApiAssociation.sourceApiAssociationStatus}`); 123 | } 124 | } -------------------------------------------------------------------------------- /src/private.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationType } from 'aws-cdk-lib/aws-appsync'; 2 | import { Directive } from './schema-base'; 3 | import { InterfaceType } from './schema-intermediate'; 4 | 5 | /** 6 | * Generates an addition to the schema 7 | * 8 | * ``` 9 | * prefix name interfaces directives { 10 | * field 11 | * field 12 | * ... 13 | * } 14 | * ``` 15 | */ 16 | export interface SchemaAdditionOptions { 17 | /** 18 | * the prefix for this additon (type, interface, enum, input, schema) 19 | */ 20 | readonly prefix: string; 21 | /** 22 | * the name for this addition (some additions dont need this [i.e. schema]) 23 | * 24 | * @default - no name 25 | */ 26 | readonly name?: string; 27 | /** 28 | * the interface types if this is creating an object type 29 | * 30 | * @default - no interfaces 31 | */ 32 | readonly interfaceTypes?: InterfaceType[]; 33 | /** 34 | * the directives for this type 35 | * 36 | * @default - no directives 37 | */ 38 | readonly directives?: Directive[]; 39 | /** 40 | * the fields to reduce onto the addition 41 | */ 42 | readonly fields: string[]; 43 | /** 44 | * the authorization modes for this graphql type 45 | */ 46 | readonly modes?: AuthorizationType[]; 47 | } 48 | 49 | /** 50 | * Generates an addition to the schema 51 | * 52 | * @param options the options to produced a stringfied addition 53 | * 54 | * @returns the following shape: 55 | * 56 | * ``` 57 | * prefix name interfaces directives { 58 | * field 59 | * field 60 | * ... 61 | * } 62 | * ``` 63 | */ 64 | export function shapeAddition(options: SchemaAdditionOptions): string { 65 | const typeName = (): string => { return options.name ? ` ${options.name}` : ''; }; 66 | const interfaces = generateInterfaces(options.interfaceTypes); 67 | const directives = generateDirectives({ 68 | directives: options.directives, 69 | modes: options.modes, 70 | }); 71 | return options.fields.reduce((acc, field) => 72 | `${acc} ${field}\n`, `${options.prefix}${typeName()}${interfaces}${directives} {\n`) + '}'; 73 | } 74 | 75 | /** 76 | * Utility class to represent DynamoDB key conditions. 77 | */ 78 | export abstract class BaseKeyCondition { 79 | public and(cond: BaseKeyCondition): BaseKeyCondition { 80 | return new (class extends BaseKeyCondition { 81 | constructor(private readonly left: BaseKeyCondition, private readonly right: BaseKeyCondition) { 82 | super(); 83 | } 84 | 85 | public renderCondition(): string { 86 | return `${this.left.renderCondition()} AND ${this.right.renderCondition()}`; 87 | } 88 | 89 | public keyNames(): string[] { 90 | return concatAndDedup(this.left.keyNames(), this.right.keyNames()); 91 | } 92 | 93 | public args(): string[] { 94 | return concatAndDedup(this.left.args(), this.right.args()); 95 | } 96 | })(this, cond); 97 | } 98 | 99 | public renderExpressionNames(): string { 100 | return this.keyNames() 101 | .map((keyName: string) => { 102 | return `"#${keyName}" : "${keyName}"`; 103 | }) 104 | .join(', '); 105 | } 106 | 107 | public renderExpressionValues(): string { 108 | return this.args() 109 | .map((arg: string) => { 110 | return `":${arg}" : $util.dynamodb.toDynamoDBJson($ctx.args.${arg})`; 111 | }) 112 | .join(', '); 113 | } 114 | 115 | public abstract renderCondition(): string; 116 | public abstract keyNames(): string[]; 117 | public abstract args(): string[]; 118 | } 119 | 120 | /** 121 | * Utility class to represent DynamoDB "begins_with" key conditions. 122 | */ 123 | export class BeginsWith extends BaseKeyCondition { 124 | constructor(private readonly keyName: string, private readonly arg: string) { 125 | super(); 126 | } 127 | 128 | public renderCondition(): string { 129 | return `begins_with(#${this.keyName}, :${this.arg})`; 130 | } 131 | 132 | public keyNames(): string[] { 133 | return [this.keyName]; 134 | } 135 | 136 | public args(): string[] { 137 | return [this.arg]; 138 | } 139 | } 140 | 141 | /** 142 | * Utility class to represent DynamoDB binary key conditions. 143 | */ 144 | export class BinaryCondition extends BaseKeyCondition { 145 | constructor(private readonly keyName: string, private readonly op: string, private readonly arg: string) { 146 | super(); 147 | } 148 | 149 | public renderCondition(): string { 150 | return `#${this.keyName} ${this.op} :${this.arg}`; 151 | } 152 | 153 | public keyNames(): string[] { 154 | return [this.keyName]; 155 | } 156 | 157 | public args(): string[] { 158 | return [this.arg]; 159 | } 160 | } 161 | 162 | /** 163 | * Utility class to represent DynamoDB "between" key conditions. 164 | */ 165 | export class Between extends BaseKeyCondition { 166 | constructor(private readonly keyName: string, private readonly arg1: string, private readonly arg2: string) { 167 | super(); 168 | } 169 | 170 | public renderCondition(): string { 171 | return `#${this.keyName} BETWEEN :${this.arg1} AND :${this.arg2}`; 172 | } 173 | 174 | public keyNames(): string[] { 175 | return [this.keyName]; 176 | } 177 | 178 | public args(): string[] { 179 | return [this.arg1, this.arg2]; 180 | } 181 | } 182 | 183 | function concatAndDedup(left: T[], right: T[]): T[] { 184 | return left.concat(right).filter((elem, index, self) => { 185 | return index === self.indexOf(elem); 186 | }); 187 | } 188 | 189 | /** 190 | * Utility function to generate interfaces for object types 191 | * 192 | * @param interfaceTypes the interfaces this object type implements 193 | */ 194 | function generateInterfaces(interfaceTypes?: InterfaceType[]): string { 195 | if (!interfaceTypes || interfaceTypes.length === 0) return ''; 196 | return interfaceTypes.reduce((acc, interfaceType) => 197 | `${acc} ${interfaceType.name} &`, ' implements').slice(0, -2); 198 | } 199 | 200 | /** 201 | * options to generate directives 202 | */ 203 | interface generateDirectivesOptions { 204 | /** 205 | * the directives of a given type 206 | */ 207 | readonly directives?: Directive[]; 208 | /** 209 | * thee separator betweeen directives 210 | * 211 | * @default - a space 212 | */ 213 | readonly delimiter?: string; 214 | /** 215 | * the authorization modes 216 | */ 217 | readonly modes?: AuthorizationType[]; 218 | } 219 | 220 | /** 221 | * Utility function to generate directives 222 | */ 223 | function generateDirectives(options: generateDirectivesOptions): string { 224 | if (!options.directives || options.directives.length === 0) return ''; 225 | // reduce over all directives and get string version of the directive 226 | // pass in the auth modes for checks to happen on compile time 227 | return options.directives.reduce((acc, directive) => 228 | `${acc}${directive._bindToAuthModes(options.modes).toString()}${options.delimiter ?? ' '}`, ' ').slice(0, -1); 229 | } 230 | -------------------------------------------------------------------------------- /src/schema-base.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, AuthorizationType, IGraphqlApi } from 'aws-cdk-lib/aws-appsync'; 2 | import { BaseTypeOptions, GraphqlType, ResolvableFieldOptions } from './schema-field'; 3 | import { InterfaceType } from './schema-intermediate'; 4 | 5 | /** 6 | * A Graphql Field 7 | */ 8 | export interface IField { 9 | /** 10 | * the type of attribute 11 | */ 12 | readonly type: Type; 13 | 14 | /** 15 | * property determining if this attribute is a list 16 | * i.e. if true, attribute would be `[Type]` 17 | * 18 | * @default false 19 | */ 20 | readonly isList: boolean; 21 | 22 | /** 23 | * property determining if this attribute is non-nullable 24 | * i.e. if true, attribute would be `Type!` and this attribute 25 | * must always have a value 26 | * 27 | * @default false 28 | */ 29 | readonly isRequired: boolean; 30 | 31 | /** 32 | * property determining if this attribute is a non-nullable list 33 | * i.e. if true, attribute would be `[ Type ]!` and this attribute's 34 | * list must always have a value 35 | * 36 | * @default false 37 | */ 38 | readonly isRequiredList: boolean; 39 | 40 | /** 41 | * The options to make this field resolvable 42 | * 43 | * @default - not a resolvable field 44 | */ 45 | readonly fieldOptions?: ResolvableFieldOptions; 46 | 47 | /** 48 | * the intermediate type linked to this attribute 49 | * (i.e. an interface or an object) 50 | * 51 | * @default - no intermediate type 52 | */ 53 | readonly intermediateType?: IIntermediateType; 54 | 55 | /** 56 | * Generate the string for this attribute 57 | */ 58 | toString(): string; 59 | 60 | /** 61 | * Generate the arguments for this field 62 | */ 63 | argsToString(): string; 64 | 65 | /** 66 | * Generate the directives for this field 67 | * 68 | * @param modes the authorization modes of the graphql api 69 | * 70 | * @default - no authorization modes 71 | */ 72 | directivesToString(modes?: AuthorizationType[]): string; 73 | } 74 | 75 | /** 76 | * The options to add a field to an Intermediate Type 77 | */ 78 | export interface AddFieldOptions { 79 | /** 80 | * The name of the field 81 | * 82 | * This option must be configured for Object, Interface, 83 | * Input and Enum Types. 84 | * 85 | * @default - no fieldName 86 | */ 87 | readonly fieldName?: string; 88 | /** 89 | * The resolvable field to add 90 | * 91 | * This option must be configured for Object, Interface, 92 | * Input and Union Types. 93 | * 94 | * @default - no IField 95 | */ 96 | readonly field?: IField; 97 | } 98 | 99 | /** 100 | * Intermediate Types are types that includes a certain set of fields 101 | * that define the entirety of your schema 102 | */ 103 | export interface IIntermediateType { 104 | /** 105 | * the name of this type 106 | */ 107 | readonly name: string; 108 | 109 | /** 110 | * the attributes of this type 111 | */ 112 | readonly definition: { [key: string]: IField }; 113 | 114 | /** 115 | * The Interface Types this Intermediate Type implements 116 | * 117 | * @default - no interface types 118 | */ 119 | readonly interfaceTypes?: InterfaceType[]; 120 | 121 | /** 122 | * the directives for this object type 123 | * 124 | * @default - no directives 125 | */ 126 | readonly directives?: Directive[]; 127 | 128 | /** 129 | * The resolvers linked to this data source 130 | */ 131 | resolvers?: Resolver[]; 132 | 133 | /** 134 | * the intermediate type linked to this attribute 135 | * (i.e. an interface or an object) 136 | * 137 | * @default - no intermediate type 138 | */ 139 | readonly intermediateType?: IIntermediateType; 140 | 141 | /** 142 | * Method called when the stringifying Intermediate Types for schema generation 143 | * 144 | * @param api The binding GraphQL Api 145 | * 146 | * @internal 147 | */ 148 | _bindToGraphqlApi(api: IGraphqlApi): IIntermediateType; 149 | 150 | /** 151 | * Create an GraphQL Type representing this Intermediate Type 152 | * 153 | * @param options the options to configure this attribute 154 | * - isList 155 | * - isRequired 156 | * - isRequiredList 157 | */ 158 | attribute(options?: BaseTypeOptions): GraphqlType; 159 | 160 | /** 161 | * Generate the string of this object type 162 | */ 163 | toString(): string; 164 | 165 | /** 166 | * Add a field to this Intermediate Type 167 | */ 168 | addField(options: AddFieldOptions): void; 169 | } 170 | 171 | interface DirectiveOptions { 172 | /** 173 | * The authorization type of this directive 174 | */ 175 | readonly mode?: AuthorizationType; 176 | 177 | /** 178 | * Mutation fields for a subscription directive 179 | */ 180 | readonly mutationFields?: string[]; 181 | } 182 | 183 | /** 184 | * Directives for types 185 | * 186 | * i.e. @aws_iam or @aws_subscribe 187 | * 188 | */ 189 | export class Directive { 190 | /** 191 | * Add the @aws_iam directive 192 | */ 193 | public static iam(): Directive { 194 | return new Directive('@aws_iam', { mode: AuthorizationType.IAM }); 195 | } 196 | 197 | /** 198 | * Add the @aws_oidc directive 199 | */ 200 | public static oidc(): Directive { 201 | return new Directive('@aws_oidc', { mode: AuthorizationType.OIDC }); 202 | } 203 | 204 | /** 205 | * Add the @aws_api_key directive 206 | */ 207 | public static apiKey(): Directive { 208 | return new Directive('@aws_api_key', { mode: AuthorizationType.API_KEY }); 209 | } 210 | 211 | /** 212 | * Add the @aws_auth or @aws_cognito_user_pools directive 213 | * 214 | * @param groups the groups to allow access to 215 | */ 216 | public static cognito(...groups: string[]): Directive { 217 | if (groups.length === 0) { 218 | throw new Error(`Cognito authorization requires at least one Cognito group to be supplied. Received: ${groups.length}`); 219 | } 220 | // this function creates the cognito groups as a string (i.e. ["group1", "group2", "group3"]) 221 | const stringify = (array: string[]): string => { 222 | return array.reduce((acc, element) => `${acc}"${element}", `, '').slice(0, -2); 223 | }; 224 | return new Directive(`@aws_auth(cognito_groups: [${stringify(groups)}])`, { mode: AuthorizationType.USER_POOL }); 225 | } 226 | 227 | /** 228 | * Add the @aws_subscribe directive. Only use for top level Subscription type. 229 | * 230 | * @param mutations the mutation fields to link to 231 | */ 232 | public static subscribe(...mutations: string[]): Directive { 233 | if (mutations.length === 0) { 234 | throw new Error(`Subscribe directive requires at least one mutation field to be supplied. Received: ${mutations.length}`); 235 | } 236 | // this function creates the subscribe directive as a string (i.e. ["mutation_field_1", "mutation_field_2"]) 237 | const stringify = (array: string[]): string => { 238 | return array.reduce((acc, mutation) => `${acc}"${mutation}", `, '').slice(0, -2); 239 | }; 240 | return new Directive(`@aws_subscribe(mutations: [${stringify(mutations)}])`, { mutationFields: mutations }); 241 | } 242 | 243 | /** 244 | * Add a custom directive 245 | * 246 | * @param statement - the directive statement to append 247 | */ 248 | public static custom(statement: string): Directive { 249 | return new Directive(statement); 250 | } 251 | 252 | /** 253 | * The authorization type of this directive 254 | * 255 | * @default - not an authorization directive 256 | */ 257 | public readonly mode?: AuthorizationType; 258 | 259 | /** 260 | * Mutation fields for a subscription directive 261 | * 262 | * @default - not a subscription directive 263 | */ 264 | public readonly mutationFields?: string[]; 265 | 266 | /** 267 | * the directive statement 268 | */ 269 | private statement: string; 270 | 271 | /** 272 | * the authorization modes for this intermediate type 273 | */ 274 | protected modes?: AuthorizationType[]; 275 | 276 | private constructor(statement: string, options?: DirectiveOptions) { 277 | this.statement = statement; 278 | this.mode = options?.mode; 279 | this.mutationFields = options?.mutationFields; 280 | } 281 | 282 | /** 283 | * Method called when the stringifying Directive for schema generation 284 | * 285 | * @param modes the authorization modes 286 | * 287 | * @internal 288 | */ 289 | public _bindToAuthModes(modes?: AuthorizationType[]): Directive { 290 | this.modes = modes; 291 | return this; 292 | } 293 | 294 | /** 295 | * Generate the directive statement 296 | */ 297 | public toString(): string { 298 | if (this.modes && this.mode && !this.modes.some((mode) => mode === this.mode)) { 299 | throw new Error(`No Authorization Type ${this.mode} declared in GraphQL Api.`); 300 | } 301 | if (this.mode === AuthorizationType.USER_POOL && this.modes && this.modes.length > 1) { 302 | this.statement = this.statement.replace('@aws_auth', '@aws_cognito_user_pools'); 303 | } 304 | return this.statement; 305 | } 306 | } 307 | 308 | /** 309 | * Enum containing the Types that can be used to define ObjectTypes 310 | */ 311 | export enum Type { 312 | /** 313 | * `ID` scalar type is a unique identifier. `ID` type is serialized similar to `String`. 314 | * 315 | * Often used as a key for a cache and not intended to be human-readable. 316 | */ 317 | ID = 'ID', 318 | /** 319 | * `String` scalar type is a free-form human-readable text. 320 | */ 321 | STRING = 'String', 322 | /** 323 | * `Int` scalar type is a signed non-fractional numerical value. 324 | */ 325 | INT = 'Int', 326 | /** 327 | * `Float` scalar type is a signed double-precision fractional value. 328 | */ 329 | FLOAT = 'Float', 330 | /** 331 | * `Boolean` scalar type is a boolean value: true or false. 332 | */ 333 | BOOLEAN = 'Boolean', 334 | 335 | /** 336 | * `AWSDate` scalar type represents a valid extended `ISO 8601 Date` string. 337 | * 338 | * In other words, accepts date strings in the form of `YYYY-MM-DD`. It accepts time zone offsets. 339 | * 340 | * @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates 341 | */ 342 | AWS_DATE = 'AWSDate', 343 | /** 344 | * `AWSTime` scalar type represents a valid extended `ISO 8601 Time` string. 345 | * 346 | * In other words, accepts date strings in the form of `hh:mm:ss.sss`. It accepts time zone offsets. 347 | * 348 | * @see https://en.wikipedia.org/wiki/ISO_8601#Times 349 | */ 350 | AWS_TIME = 'AWSTime', 351 | /** 352 | * `AWSDateTime` scalar type represents a valid extended `ISO 8601 DateTime` string. 353 | * 354 | * In other words, accepts date strings in the form of `YYYY-MM-DDThh:mm:ss.sssZ`. It accepts time zone offsets. 355 | * 356 | * @see https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations 357 | */ 358 | AWS_DATE_TIME = 'AWSDateTime', 359 | /** 360 | * `AWSTimestamp` scalar type represents the number of seconds since `1970-01-01T00:00Z`. 361 | * 362 | * Timestamps are serialized and deserialized as numbers. 363 | */ 364 | AWS_TIMESTAMP = 'AWSTimestamp', 365 | /** 366 | * `AWSEmail` scalar type represents an email address string (i.e.`username@example.com`) 367 | */ 368 | AWS_EMAIL = 'AWSEmail', 369 | /** 370 | * `AWSJson` scalar type represents a JSON string. 371 | */ 372 | AWS_JSON = 'AWSJSON', 373 | /** 374 | * `AWSURL` scalar type represetns a valid URL string. 375 | * 376 | * URLs wihtout schemes or contain double slashes are considered invalid. 377 | */ 378 | AWS_URL = 'AWSURL', 379 | /** 380 | * `AWSPhone` scalar type represents a valid phone number. Phone numbers maybe be whitespace delimited or hyphenated. 381 | * 382 | * The number can specify a country code at the beginning, but is not required for US phone numbers. 383 | */ 384 | AWS_PHONE = 'AWSPhone', 385 | /** 386 | * `AWSIPAddress` scalar type respresents a valid `IPv4` of `IPv6` address string. 387 | */ 388 | AWS_IP_ADDRESS = 'AWSIPAddress', 389 | 390 | /** 391 | * Type used for Intermediate Types 392 | * (i.e. an interface or an object type) 393 | */ 394 | INTERMEDIATE = 'INTERMEDIATE', 395 | } 396 | -------------------------------------------------------------------------------- /src/schema-field.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IAppsyncFunction, 3 | BaseDataSource, 4 | AuthorizationType, 5 | MappingTemplate, 6 | CachingConfig, 7 | FunctionRuntime, 8 | Code, 9 | } from 'aws-cdk-lib/aws-appsync'; 10 | import { Type, IField, IIntermediateType, Directive } from './schema-base'; 11 | 12 | /** 13 | * Base options for GraphQL Types 14 | * 15 | * @option isList - is this attribute a list 16 | * @option isRequired - is this attribute non-nullable 17 | * @option isRequiredList - is this attribute a non-nullable list 18 | * 19 | */ 20 | export interface BaseTypeOptions { 21 | /** 22 | * property determining if this attribute is a list 23 | * i.e. if true, attribute would be [Type] 24 | * 25 | * @default - false 26 | */ 27 | readonly isList?: boolean; 28 | 29 | /** 30 | * property determining if this attribute is non-nullable 31 | * i.e. if true, attribute would be Type! 32 | * 33 | * @default - false 34 | */ 35 | readonly isRequired?: boolean; 36 | 37 | /** 38 | * property determining if this attribute is a non-nullable list 39 | * i.e. if true, attribute would be [ Type ]! 40 | * or if isRequired true, attribe would be [ Type! ]! 41 | * 42 | * @default - false 43 | */ 44 | readonly isRequiredList?: boolean; 45 | } 46 | 47 | /** 48 | * Options for GraphQL Types 49 | * 50 | * @option isList - is this attribute a list 51 | * @option isRequired - is this attribute non-nullable 52 | * @option isRequiredList - is this attribute a non-nullable list 53 | * @option objectType - the object type linked to this attribute 54 | * 55 | */ 56 | export interface GraphqlTypeOptions extends BaseTypeOptions { 57 | /** 58 | * the intermediate type linked to this attribute 59 | * @default - no intermediate type 60 | */ 61 | readonly intermediateType?: IIntermediateType; 62 | } 63 | 64 | /** 65 | * The GraphQL Types in AppSync's GraphQL. GraphQL Types are the 66 | * building blocks for object types, queries, mutations, etc. They are 67 | * types like String, Int, Id or even Object Types you create. 68 | * 69 | * i.e. `String`, `String!`, `[String]`, `[String!]`, `[String]!` 70 | * 71 | * GraphQL Types are used to define the entirety of schema. 72 | */ 73 | export class GraphqlType implements IField { 74 | /** 75 | * `ID` scalar type is a unique identifier. `ID` type is serialized similar to `String`. 76 | * 77 | * Often used as a key for a cache and not intended to be human-readable. 78 | * 79 | * @param options the options to configure this attribute 80 | * - isList 81 | * - isRequired 82 | * - isRequiredList 83 | */ 84 | public static id(options?: BaseTypeOptions): GraphqlType { 85 | return new GraphqlType(Type.ID, options); 86 | } 87 | /** 88 | * `String` scalar type is a free-form human-readable text. 89 | * 90 | * @param options the options to configure this attribute 91 | * - isList 92 | * - isRequired 93 | * - isRequiredList 94 | */ 95 | public static string(options?: BaseTypeOptions): GraphqlType { 96 | return new GraphqlType(Type.STRING, options); 97 | } 98 | /** 99 | * `Int` scalar type is a signed non-fractional numerical value. 100 | * 101 | * @param options the options to configure this attribute 102 | * - isList 103 | * - isRequired 104 | * - isRequiredList 105 | */ 106 | public static int(options?: BaseTypeOptions): GraphqlType { 107 | return new GraphqlType(Type.INT, options); 108 | } 109 | /** 110 | * `Float` scalar type is a signed double-precision fractional value. 111 | * 112 | * @param options the options to configure this attribute 113 | * - isList 114 | * - isRequired 115 | * - isRequiredList 116 | */ 117 | public static float(options?: BaseTypeOptions): GraphqlType { 118 | return new GraphqlType(Type.FLOAT, options); 119 | } 120 | /** 121 | * `Boolean` scalar type is a boolean value: true or false. 122 | * 123 | * @param options the options to configure this attribute 124 | * - isList 125 | * - isRequired 126 | * - isRequiredList 127 | */ 128 | public static boolean(options?: BaseTypeOptions): GraphqlType { 129 | return new GraphqlType(Type.BOOLEAN, options); 130 | } 131 | 132 | /** 133 | * `AWSDate` scalar type represents a valid extended `ISO 8601 Date` string. 134 | * 135 | * In other words, accepts date strings in the form of `YYYY-MM-DD`. It accepts time zone offsets. 136 | * 137 | * @param options the options to configure this attribute 138 | * - isList 139 | * - isRequired 140 | * - isRequiredList 141 | */ 142 | public static awsDate(options?: BaseTypeOptions): GraphqlType { 143 | return new GraphqlType(Type.AWS_DATE, options); 144 | } 145 | /** 146 | * `AWSTime` scalar type represents a valid extended `ISO 8601 Time` string. 147 | * 148 | * In other words, accepts date strings in the form of `hh:mm:ss.sss`. It accepts time zone offsets. 149 | * 150 | * @param options the options to configure this attribute 151 | * - isList 152 | * - isRequired 153 | * - isRequiredList 154 | */ 155 | public static awsTime(options?: BaseTypeOptions): GraphqlType { 156 | return new GraphqlType(Type.AWS_TIME, options); 157 | } 158 | /** 159 | * `AWSDateTime` scalar type represents a valid extended `ISO 8601 DateTime` string. 160 | * 161 | * In other words, accepts date strings in the form of `YYYY-MM-DDThh:mm:ss.sssZ`. It accepts time zone offsets. 162 | * 163 | * @param options the options to configure this attribute 164 | * - isList 165 | * - isRequired 166 | * - isRequiredList 167 | */ 168 | public static awsDateTime(options?: BaseTypeOptions): GraphqlType { 169 | return new GraphqlType(Type.AWS_DATE_TIME, options); 170 | } 171 | /** 172 | * `AWSTimestamp` scalar type represents the number of seconds since `1970-01-01T00:00Z`. 173 | * 174 | * Timestamps are serialized and deserialized as numbers. 175 | * 176 | * @param options the options to configure this attribute 177 | * - isList 178 | * - isRequired 179 | * - isRequiredList 180 | */ 181 | public static awsTimestamp(options?: BaseTypeOptions): GraphqlType { 182 | return new GraphqlType(Type.AWS_TIMESTAMP, options); 183 | } 184 | /** 185 | * `AWSEmail` scalar type represents an email address string (i.e.`username@example.com`) 186 | * 187 | * @param options the options to configure this attribute 188 | * - isList 189 | * - isRequired 190 | * - isRequiredList 191 | */ 192 | public static awsEmail(options?: BaseTypeOptions): GraphqlType { 193 | return new GraphqlType(Type.AWS_EMAIL, options); 194 | } 195 | /** 196 | * `AWSJson` scalar type represents a JSON string. 197 | * 198 | * @param options the options to configure this attribute 199 | * - isList 200 | * - isRequired 201 | * - isRequiredList 202 | */ 203 | public static awsJson(options?: BaseTypeOptions): GraphqlType { 204 | return new GraphqlType(Type.AWS_JSON, options); 205 | } 206 | /** 207 | * `AWSURL` scalar type represetns a valid URL string. 208 | * 209 | * URLs wihtout schemes or contain double slashes are considered invalid. 210 | * 211 | * @param options the options to configure this attribute 212 | * - isList 213 | * - isRequired 214 | * - isRequiredList 215 | */ 216 | public static awsUrl(options?: BaseTypeOptions): GraphqlType { 217 | return new GraphqlType(Type.AWS_URL, options); 218 | } 219 | /** 220 | * `AWSPhone` scalar type represents a valid phone number. Phone numbers maybe be whitespace delimited or hyphenated. 221 | * 222 | * The number can specify a country code at the beginning, but is not required for US phone numbers. 223 | * 224 | * @param options the options to configure this attribute 225 | * - isList 226 | * - isRequired 227 | * - isRequiredList 228 | */ 229 | public static awsPhone(options?: BaseTypeOptions): GraphqlType { 230 | return new GraphqlType(Type.AWS_PHONE, options); 231 | } 232 | /** 233 | * `AWSIPAddress` scalar type respresents a valid `IPv4` of `IPv6` address string. 234 | * 235 | * @param options the options to configure this attribute 236 | * - isList 237 | * - isRequired 238 | * - isRequiredList 239 | */ 240 | public static awsIpAddress(options?: BaseTypeOptions): GraphqlType { 241 | return new GraphqlType(Type.AWS_IP_ADDRESS, options); 242 | } 243 | 244 | /** 245 | * an intermediate type to be added as an attribute 246 | * (i.e. an interface or an object type) 247 | * 248 | * @param options the options to configure this attribute 249 | * - isList 250 | * - isRequired 251 | * - isRequiredList 252 | * - intermediateType 253 | */ 254 | public static intermediate(options?: GraphqlTypeOptions): GraphqlType { 255 | if (!options?.intermediateType) { 256 | throw new Error('GraphQL Type of interface must be configured with corresponding Intermediate Type'); 257 | } 258 | return new GraphqlType(Type.INTERMEDIATE, options); 259 | } 260 | 261 | /** 262 | * the type of attribute 263 | */ 264 | public readonly type: Type; 265 | 266 | /** 267 | * property determining if this attribute is a list 268 | * i.e. if true, attribute would be `[Type]` 269 | * 270 | * @default - false 271 | */ 272 | public readonly isList: boolean; 273 | 274 | /** 275 | * property determining if this attribute is non-nullable 276 | * i.e. if true, attribute would be `Type!` and this attribute 277 | * must always have a value 278 | * 279 | * @default - false 280 | */ 281 | public readonly isRequired: boolean; 282 | 283 | /** 284 | * property determining if this attribute is a non-nullable list 285 | * i.e. if true, attribute would be `[ Type ]!` and this attribute's 286 | * list must always have a value 287 | * 288 | * @default - false 289 | */ 290 | public readonly isRequiredList: boolean; 291 | 292 | /** 293 | * the intermediate type linked to this attribute 294 | * (i.e. an interface or an object) 295 | * 296 | * @default - no intermediate type 297 | */ 298 | public readonly intermediateType?: IIntermediateType; 299 | 300 | protected constructor(type: Type, options?: GraphqlTypeOptions) { 301 | this.type = type; 302 | this.isList = options?.isList ?? false; 303 | this.isRequired = options?.isRequired ?? false; 304 | this.isRequiredList = options?.isRequiredList ?? false; 305 | this.intermediateType = options?.intermediateType; 306 | } 307 | 308 | /** 309 | * Generate the string for this attribute 310 | */ 311 | public toString(): string { 312 | // If an Object Type, we use the name of the Object Type 313 | let type = this.intermediateType ? this.intermediateType?.name : this.type; 314 | // If configured as required, the GraphQL Type becomes required 315 | type = this.isRequired ? `${type}!` : type; 316 | // If configured with isXxxList, the GraphQL Type becomes a list 317 | type = this.isList || this.isRequiredList ? `[${type}]` : type; 318 | // If configured with isRequiredList, the list becomes required 319 | type = this.isRequiredList ? `${type}!` : type; 320 | return type; 321 | } 322 | 323 | /** 324 | * Generate the arguments for this field 325 | */ 326 | public argsToString(): string { 327 | return ''; 328 | } 329 | 330 | /** 331 | * Generate the directives for this field 332 | */ 333 | public directivesToString(_modes?: AuthorizationType[]): string { 334 | return ''; 335 | } 336 | } 337 | 338 | /** 339 | * Properties for configuring a field 340 | * 341 | * @options args - the variables and types that define the arguments 342 | * 343 | * i.e. { string: GraphqlType, string: GraphqlType } 344 | */ 345 | export interface FieldOptions { 346 | /** 347 | * The return type for this field 348 | */ 349 | readonly returnType: GraphqlType; 350 | /** 351 | * The arguments for this field. 352 | * 353 | * i.e. type Example (first: String second: String) {} 354 | * - where 'first' and 'second' are key values for args 355 | * and 'String' is the GraphqlType 356 | * 357 | * @default - no arguments 358 | */ 359 | readonly args?: { [key: string]: GraphqlType }; 360 | /** 361 | * the directives for this field 362 | * 363 | * @default - no directives 364 | */ 365 | readonly directives?: Directive[]; 366 | } 367 | 368 | /** 369 | * Fields build upon Graphql Types and provide typing 370 | * and arguments. 371 | */ 372 | export class Field extends GraphqlType implements IField { 373 | /** 374 | * The options for this field 375 | * 376 | * @default - no arguments 377 | */ 378 | public readonly fieldOptions?: ResolvableFieldOptions; 379 | 380 | public constructor(options: FieldOptions) { 381 | const props = { 382 | isList: options.returnType.isList, 383 | isRequired: options.returnType.isRequired, 384 | isRequiredList: options.returnType.isRequiredList, 385 | intermediateType: options.returnType.intermediateType, 386 | }; 387 | super(options.returnType.type, props); 388 | this.fieldOptions = options; 389 | } 390 | 391 | /** 392 | * Generate the args string of this resolvable field 393 | */ 394 | public argsToString(): string { 395 | if (!this.fieldOptions || !this.fieldOptions.args) { return ''; } 396 | return Object.keys(this.fieldOptions.args).reduce((acc, key) => 397 | `${acc}${key}: ${this.fieldOptions?.args?.[key].toString()} `, '(').slice(0, -1) + ')'; 398 | } 399 | 400 | /** 401 | * Generate the directives for this field 402 | */ 403 | public directivesToString(modes?: AuthorizationType[]): string { 404 | if (!this.fieldOptions || !this.fieldOptions.directives) { return ''; } 405 | return this.fieldOptions.directives.reduce((acc, directive) => 406 | `${acc}${directive._bindToAuthModes(modes).toString()} `, '\n ').slice(0, -1); 407 | } 408 | } 409 | 410 | /** 411 | * Properties for configuring a resolvable field 412 | * 413 | * @options dataSource - the data source linked to this resolvable field 414 | * @options requestMappingTemplate - the mapping template for requests to this resolver 415 | * @options responseMappingTemplate - the mapping template for responses from this resolver 416 | */ 417 | export interface ResolvableFieldOptions extends FieldOptions { 418 | /** 419 | * The data source creating linked to this resolvable field 420 | * 421 | * @default - no data source 422 | */ 423 | readonly dataSource?: BaseDataSource; 424 | /** 425 | * configuration of the pipeline resolver 426 | * 427 | * @default - no pipeline resolver configuration 428 | * An empty array | undefined sets resolver to be of kind, unit 429 | */ 430 | readonly pipelineConfig?: IAppsyncFunction[]; 431 | /** 432 | * The request mapping template for this resolver 433 | * 434 | * @default - No mapping template 435 | */ 436 | readonly requestMappingTemplate?: MappingTemplate; 437 | /** 438 | * The response mapping template for this resolver 439 | * 440 | * @default - No mapping template 441 | */ 442 | readonly responseMappingTemplate?: MappingTemplate; 443 | /** 444 | * The caching configuration for this resolver 445 | * 446 | * @default - No caching configuration 447 | */ 448 | readonly cachingConfig?: CachingConfig; 449 | /** 450 | * The maximum number of elements per batch, when using batch invoke 451 | * 452 | * @default - No max batch size 453 | */ 454 | readonly maxBatchSize?: number; 455 | /** 456 | * The functions runtime 457 | * 458 | * @default - no function runtime, VTL mapping templates used 459 | */ 460 | readonly runtime?: FunctionRuntime; 461 | /** 462 | * The function code 463 | * 464 | * @default - no code is used 465 | */ 466 | readonly code?: Code; 467 | } 468 | 469 | /** 470 | * Resolvable Fields build upon Graphql Types and provide fields 471 | * that can resolve into operations on a data source. 472 | */ 473 | export class ResolvableField extends Field implements IField { 474 | /** 475 | * The options to make this field resolvable 476 | * 477 | * @default - not a resolvable field 478 | */ 479 | public readonly fieldOptions?: ResolvableFieldOptions; 480 | 481 | public constructor(options: ResolvableFieldOptions) { 482 | const props = { 483 | returnType: options.returnType, 484 | args: options.args, 485 | }; 486 | super(props); 487 | this.fieldOptions = options; 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /src/schema-intermediate.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, IGraphqlApi, AuthorizationType, GraphqlApi } from 'aws-cdk-lib/aws-appsync'; 2 | import { shapeAddition } from './private'; 3 | import { Directive, IField, IIntermediateType, AddFieldOptions } from './schema-base'; 4 | import { BaseTypeOptions, GraphqlType, ResolvableFieldOptions, ResolvableField } from './schema-field'; 5 | 6 | /** 7 | * Properties for configuring an Intermediate Type 8 | * 9 | * @param definition - the variables and types that define this type 10 | * i.e. { string: GraphqlType, string: GraphqlType } 11 | * @param directives - the directives for this object type 12 | * 13 | */ 14 | export interface IntermediateTypeOptions { 15 | /** 16 | * the attributes of this type 17 | */ 18 | readonly definition: { [key: string]: IField }; 19 | /** 20 | * the directives for this object type 21 | * 22 | * @default - no directives 23 | */ 24 | readonly directives?: Directive[]; 25 | } 26 | 27 | /** 28 | * Interface Types are abstract types that includes a certain set of fields 29 | * that other types must include if they implement the interface. 30 | * 31 | */ 32 | export class InterfaceType implements IIntermediateType { 33 | /** 34 | * the name of this type 35 | */ 36 | public readonly name: string; 37 | /** 38 | * the attributes of this type 39 | */ 40 | public readonly definition: { [key: string]: IField }; 41 | /** 42 | * the directives for this object type 43 | * 44 | * @default - no directives 45 | */ 46 | public readonly directives?: Directive[]; 47 | /** 48 | * the authorization modes for this intermediate type 49 | */ 50 | protected modes?: AuthorizationType[]; 51 | 52 | public constructor(name: string, props: IntermediateTypeOptions) { 53 | this.name = name; 54 | this.definition = props.definition; 55 | this.directives = props.directives; 56 | } 57 | 58 | /** 59 | * Create a GraphQL Type representing this Intermediate Type 60 | * 61 | * @param options the options to configure this attribute 62 | */ 63 | public attribute(options?: BaseTypeOptions): GraphqlType { 64 | return GraphqlType.intermediate({ 65 | isList: options?.isList, 66 | isRequired: options?.isRequired, 67 | isRequiredList: options?.isRequiredList, 68 | intermediateType: this, 69 | }); 70 | } 71 | 72 | /** 73 | * Generate the string of this object type 74 | */ 75 | public toString(): string { 76 | return shapeAddition({ 77 | prefix: 'interface', 78 | name: this.name, 79 | directives: this.directives, 80 | fields: Object.keys(this.definition).map((key) => { 81 | const field = this.definition[key]; 82 | return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(this.modes)}`; 83 | }), 84 | modes: this.modes, 85 | }); 86 | } 87 | 88 | /** 89 | * Add a field to this Interface Type. 90 | * 91 | * Interface Types must have both fieldName and field options. 92 | * 93 | * @param options the options to add a field 94 | */ 95 | public addField(options: AddFieldOptions): void { 96 | if (!options.fieldName || !options.field) { 97 | throw new Error('Interface Types must have both fieldName and field options.'); 98 | } 99 | this.definition[options.fieldName] = options.field; 100 | } 101 | 102 | /** 103 | * Method called when the stringifying Intermediate Types for schema generation 104 | * 105 | * @internal 106 | */ 107 | public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { 108 | this.modes = api.modes; 109 | return this; 110 | } 111 | } 112 | 113 | /** 114 | * Properties for configuring an Object Type 115 | * 116 | * @param definition - the variables and types that define this type 117 | * i.e. { string: GraphqlType, string: GraphqlType } 118 | * @param interfaceTypes - the interfaces that this object type implements 119 | * @param directives - the directives for this object type 120 | * 121 | */ 122 | export interface ObjectTypeOptions extends IntermediateTypeOptions { 123 | /** 124 | * The Interface Types this Object Type implements 125 | * 126 | * @default - no interface types 127 | */ 128 | readonly interfaceTypes?: InterfaceType[]; 129 | } 130 | 131 | /** 132 | * Object Types are types declared by you. 133 | * 134 | */ 135 | export class ObjectType extends InterfaceType implements IIntermediateType { 136 | /** 137 | * The Interface Types this Object Type implements 138 | * 139 | * @default - no interface types 140 | */ 141 | public readonly interfaceTypes?: InterfaceType[]; 142 | /** 143 | * The resolvers linked to this data source 144 | */ 145 | public resolvers?: Resolver[]; 146 | 147 | public constructor(name: string, props: ObjectTypeOptions) { 148 | const options = { 149 | definition: props.interfaceTypes?.reduce((def, interfaceType) => { 150 | return Object.assign({}, def, interfaceType.definition); 151 | }, props.definition) ?? props.definition, 152 | directives: props.directives, 153 | }; 154 | super(name, options); 155 | this.interfaceTypes = props.interfaceTypes; 156 | this.resolvers = []; 157 | } 158 | 159 | /** 160 | * Method called when the stringifying Intermediate Types for schema generation 161 | * 162 | * @internal 163 | */ 164 | public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { 165 | this.modes = api.modes; 166 | // If the resolvers have been generated, skip the bind 167 | if (this.resolvers && this.resolvers.length > 0) { 168 | return this; 169 | } 170 | Object.keys(this.definition).forEach((fieldName) => { 171 | const field = this.definition[fieldName]; 172 | if (field instanceof ResolvableField) { 173 | if (!this.resolvers) this.resolvers = []; 174 | this.resolvers.push(this.generateResolver(api, fieldName, field.fieldOptions)); 175 | } 176 | }); 177 | return this; 178 | } 179 | 180 | /** 181 | * Add a field to this Object Type. 182 | * 183 | * Object Types must have both fieldName and field options. 184 | * 185 | * @param options the options to add a field 186 | */ 187 | public addField(options: AddFieldOptions): void { 188 | if (!options.fieldName || !options.field) { 189 | throw new Error('Object Types must have both fieldName and field options.'); 190 | } 191 | this.definition[options.fieldName] = options.field; 192 | } 193 | 194 | /** 195 | * Generate the string of this object type 196 | */ 197 | public toString(): string { 198 | return shapeAddition({ 199 | prefix: 'type', 200 | name: this.name, 201 | interfaceTypes: this.interfaceTypes, 202 | directives: this.directives, 203 | fields: Object.keys(this.definition).map((key) => { 204 | const field = this.definition[key]; 205 | return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(this.modes)}`; 206 | }), 207 | modes: this.modes, 208 | }); 209 | } 210 | 211 | /** 212 | * Generate the resolvers linked to this Object Type 213 | */ 214 | protected generateResolver(api: IGraphqlApi, fieldName: string, options?: ResolvableFieldOptions): Resolver { 215 | return api.createResolver(`${this.name}${fieldName}Resolver`, { 216 | typeName: this.name, 217 | fieldName: fieldName, 218 | ...options, 219 | }); 220 | } 221 | } 222 | 223 | /** 224 | * Input Types are abstract types that define complex objects. 225 | * They are used in arguments to represent 226 | * 227 | */ 228 | export class InputType implements IIntermediateType { 229 | /** 230 | * the name of this type 231 | */ 232 | public readonly name: string; 233 | /** 234 | * the attributes of this type 235 | */ 236 | public readonly definition: { [key: string]: IField }; 237 | /** 238 | * the authorization modes for this intermediate type 239 | */ 240 | protected modes?: AuthorizationType[]; 241 | 242 | public constructor(name: string, props: IntermediateTypeOptions) { 243 | this.name = name; 244 | this.definition = props.definition; 245 | } 246 | 247 | /** 248 | * Create a GraphQL Type representing this Input Type 249 | * 250 | * @param options the options to configure this attribute 251 | */ 252 | public attribute(options?: BaseTypeOptions): GraphqlType { 253 | return GraphqlType.intermediate({ 254 | isList: options?.isList, 255 | isRequired: options?.isRequired, 256 | isRequiredList: options?.isRequiredList, 257 | intermediateType: this, 258 | }); 259 | } 260 | 261 | /** 262 | * Generate the string of this input type 263 | */ 264 | public toString(): string { 265 | return shapeAddition({ 266 | prefix: 'input', 267 | name: this.name, 268 | fields: Object.keys(this.definition).map((key) => 269 | `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), 270 | modes: this.modes, 271 | }); 272 | } 273 | 274 | /** 275 | * Add a field to this Input Type. 276 | * 277 | * Input Types must have both fieldName and field options. 278 | * 279 | * @param options the options to add a field 280 | */ 281 | public addField(options: AddFieldOptions): void { 282 | if (!options.fieldName || !options.field) { 283 | throw new Error('Input Types must have both fieldName and field options.'); 284 | } 285 | this.definition[options.fieldName] = options.field; 286 | } 287 | 288 | /** 289 | * Method called when the stringifying Intermediate Types for schema generation 290 | * 291 | * @internal 292 | */ 293 | public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { 294 | this.modes = api.modes; 295 | return this; 296 | } 297 | } 298 | 299 | /** 300 | * Properties for configuring an Union Type 301 | * 302 | */ 303 | export interface UnionTypeOptions { 304 | /** 305 | * the object types for this union type 306 | */ 307 | readonly definition: IIntermediateType[]; 308 | } 309 | 310 | /** 311 | * Union Types are abstract types that are similar to Interface Types, 312 | * but they cannot to specify any common fields between types. 313 | * 314 | * Note that fields of a union type need to be object types. In other words, 315 | * you can't create a union type out of interfaces, other unions, or inputs. 316 | * 317 | */ 318 | export class UnionType implements IIntermediateType { 319 | /** 320 | * the name of this type 321 | */ 322 | public readonly name: string; 323 | /** 324 | * the attributes of this type 325 | */ 326 | public readonly definition: { [key: string]: IField }; 327 | /** 328 | * the authorization modes supported by this intermediate type 329 | */ 330 | protected modes?: AuthorizationType[]; 331 | 332 | public constructor(name: string, options: UnionTypeOptions) { 333 | this.name = name; 334 | this.definition = {}; 335 | options.definition.map((def) => this.addField({ field: def.attribute() })); 336 | } 337 | 338 | /** 339 | * Create a GraphQL Type representing this Union Type 340 | * 341 | * @param options the options to configure this attribute 342 | */ 343 | public attribute(options?: BaseTypeOptions): GraphqlType { 344 | return GraphqlType.intermediate({ 345 | isList: options?.isList, 346 | isRequired: options?.isRequired, 347 | isRequiredList: options?.isRequiredList, 348 | intermediateType: this, 349 | }); 350 | } 351 | 352 | /** 353 | * Generate the string of this Union type 354 | */ 355 | public toString(): string { 356 | // Return a string that appends all Object Types for this Union Type 357 | // i.e. 'union Example = example1 | example2' 358 | return Object.values(this.definition).reduce((acc, field) => 359 | `${acc} ${field.toString()} |`, `union ${this.name} =`).slice(0, -2); 360 | } 361 | 362 | /** 363 | * Add a field to this Union Type 364 | * 365 | * Input Types must have field options and the IField must be an Object Type. 366 | * 367 | * @param options the options to add a field 368 | */ 369 | public addField(options: AddFieldOptions): void { 370 | if (options.fieldName) { 371 | throw new Error('Union Types cannot be configured with the fieldName option. Use the field option instead.'); 372 | } 373 | if (!options.field) { 374 | throw new Error('Union Types must be configured with the field option.'); 375 | } 376 | if (options.field && !(options.field.intermediateType instanceof ObjectType)) { 377 | throw new Error('Fields for Union Types must be Object Types.'); 378 | } 379 | this.definition[options.field.toString()] = options.field; 380 | } 381 | 382 | /** 383 | * Method called when the stringifying Intermediate Types for schema generation 384 | * 385 | * @internal 386 | */ 387 | public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { 388 | this.modes = api.modes; 389 | return this; 390 | } 391 | } 392 | 393 | /** 394 | * Properties for configuring an Enum Type 395 | * 396 | */ 397 | export interface EnumTypeOptions { 398 | /** 399 | * the attributes of this type 400 | */ 401 | readonly definition: string[]; 402 | } 403 | 404 | /** 405 | * Enum Types are abstract types that includes a set of fields 406 | * that represent the strings this type can create. 407 | * 408 | */ 409 | export class EnumType implements IIntermediateType { 410 | /** 411 | * the name of this type 412 | */ 413 | public readonly name: string; 414 | /** 415 | * the attributes of this type 416 | */ 417 | public readonly definition: { [key: string]: IField }; 418 | /** 419 | * the authorization modes for this intermediate type 420 | */ 421 | protected modes?: AuthorizationType[]; 422 | 423 | public constructor(name: string, options: EnumTypeOptions) { 424 | this.name = name; 425 | this.definition = {}; 426 | options.definition.map((fieldName: string) => this.addField({ fieldName })); 427 | } 428 | 429 | /** 430 | * Create an GraphQL Type representing this Enum Type 431 | */ 432 | public attribute(options?: BaseTypeOptions): GraphqlType { 433 | return GraphqlType.intermediate({ 434 | isList: options?.isList, 435 | isRequired: options?.isRequired, 436 | isRequiredList: options?.isRequiredList, 437 | intermediateType: this, 438 | }); 439 | } 440 | 441 | /** 442 | * Generate the string of this enum type 443 | */ 444 | public toString(): string { 445 | return shapeAddition({ 446 | prefix: 'enum', 447 | name: this.name, 448 | fields: Object.keys(this.definition), 449 | modes: this.modes, 450 | }); 451 | } 452 | 453 | /** 454 | * Add a field to this Enum Type 455 | * 456 | * To add a field to this Enum Type, you must only configure 457 | * addField with the fieldName options. 458 | * 459 | * @param options the options to add a field 460 | */ 461 | public addField(options: AddFieldOptions): void { 462 | if (options.field) { 463 | throw new Error('Enum Type fields consist of strings. Use the fieldName option instead of the field option.'); 464 | } 465 | if (!options.fieldName) { 466 | throw new Error('When adding a field to an Enum Type, you must configure the fieldName option.'); 467 | } 468 | if (options.fieldName.indexOf(' ') > -1) { 469 | throw new Error(`Enum Type values cannot have whitespace. Received: ${options.fieldName}`); 470 | } 471 | this.definition[options.fieldName] = GraphqlType.string(); 472 | } 473 | 474 | /** 475 | * Method called when the stringifying Intermediate Types for schema generation 476 | * 477 | * @internal 478 | */ 479 | public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { 480 | this.modes = api.modes; 481 | return this; 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/source-api-association-merge.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import * as path from 'path'; 3 | import { CfnResource, CustomResource, Duration, Stack } from 'aws-cdk-lib'; 4 | import { ISourceApiAssociation } from 'aws-cdk-lib/aws-appsync'; 5 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 6 | import { Code, Runtime, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; 7 | import { Provider } from 'aws-cdk-lib/custom-resources'; 8 | import { Construct, IConstruct } from 'constructs'; 9 | 10 | /** 11 | * This interface for the provider of the custom resource that will be used to initiate a merge operation during Cloudformation update. 12 | */ 13 | export interface ISourceApiAssociationMergeOperationProvider extends IConstruct { 14 | 15 | /** 16 | * Service token which is used for identifying the handler used for the merge operation custom resource. 17 | */ 18 | readonly serviceToken: string; 19 | 20 | /** 21 | * This function associates a source api association with the provider. 22 | * This method can be used for adding permissions to merge a specific source api association to the custom resource provider. 23 | * @param sourceApiAssociation The association to associate. 24 | */ 25 | associateSourceApiAssociation(sourceApiAssociation: ISourceApiAssociation): void; 26 | } 27 | 28 | /** 29 | * Properties for SourceApiAssociationMergeOperationProvider. 30 | */ 31 | export interface SourceApiAssociationMergeOperationProviderProps { 32 | 33 | /** 34 | * Time between calls to the polling Lambda function which determines whether the merge operation is finished or not. 35 | * 36 | * @default Duration.seconds(5) 37 | */ 38 | readonly pollingInterval?: Duration; 39 | 40 | /** 41 | * Total timeout in waiting for the source api association merge operation to complete. 42 | * 43 | * @default Duration.minutes(15) 44 | */ 45 | readonly totalTimeout?: Duration; 46 | } 47 | 48 | /** 49 | * SourceApiAssociationMergeProvider class is responsible for constructing the custom resource that will be used for initiating the 50 | * source API merge during a Cloudformation update. 51 | */ 52 | export class SourceApiAssociationMergeOperationProvider extends Construct implements ISourceApiAssociationMergeOperationProvider { 53 | 54 | /** 55 | * Service token for the resource provider. 56 | */ 57 | public readonly serviceToken: string; 58 | 59 | /** 60 | * The lambda function responsible for kicking off the merge operation. 61 | */ 62 | public readonly schemaMergeLambda: SingletonFunction; 63 | 64 | /** 65 | * The lambda function response for ensuring that the merge operation finished. 66 | */ 67 | public readonly sourceApiStablizationLambda: SingletonFunction; 68 | 69 | constructor(scope: Construct, id: string, props: SourceApiAssociationMergeOperationProviderProps) { 70 | super(scope, id); 71 | 72 | this.schemaMergeLambda = new SingletonFunction(this, 'MergeSourceApiSchemaLambda', { 73 | runtime: Runtime.NODEJS_20_X, 74 | code: Code.fromAsset(path.join(__dirname, 'mergeSourceApiSchemaHandler')), 75 | handler: 'index.onEvent', 76 | timeout: Duration.minutes(2), 77 | uuid: '6148f39b-95bb-47e7-8a35-40adb8b93a7b', 78 | }); 79 | 80 | this.sourceApiStablizationLambda = new SingletonFunction(this, 'PollSourceApiMergeLambda', { 81 | runtime: Runtime.NODEJS_20_X, 82 | code: Code.fromAsset(path.join(__dirname, 'mergeSourceApiSchemaHandler')), 83 | handler: 'index.isComplete', 84 | timeout: Duration.minutes(2), 85 | uuid: '163e01ec-6f29-4bf4-b3b1-11245b00a6bc', 86 | }); 87 | 88 | const provider = new Provider(this, 'SchemaMergeOperationProvider', { 89 | onEventHandler: this.schemaMergeLambda, 90 | isCompleteHandler: this.sourceApiStablizationLambda, 91 | queryInterval: props.pollingInterval ?? Duration.seconds(5), 92 | totalTimeout: props.totalTimeout ?? Duration.minutes(15), 93 | }); 94 | 95 | this.serviceToken = provider.serviceToken; 96 | } 97 | 98 | public associateSourceApiAssociation(sourceApiAssociation: ISourceApiAssociation) { 99 | this.schemaMergeLambda.addToRolePolicy(new PolicyStatement({ 100 | effect: Effect.ALLOW, 101 | resources: [sourceApiAssociation.associationArn], 102 | actions: ['appsync:StartSchemaMerge'], 103 | })); 104 | 105 | this.sourceApiStablizationLambda.addToRolePolicy(new PolicyStatement({ 106 | effect: Effect.ALLOW, 107 | resources: [sourceApiAssociation.associationArn], 108 | actions: ['appsync:GetSourceApiAssociation'], 109 | })); 110 | } 111 | } 112 | 113 | /** 114 | * Properties for SourceApiAssociationMergeOperation which handles triggering a merge operation as a custom resource 115 | * during a Cloudformation stack update. 116 | */ 117 | export interface SourceApiAssociationMergeOperationProps { 118 | 119 | /** 120 | * The source api association resource which will be merged. 121 | */ 122 | readonly sourceApiAssociation: ISourceApiAssociation; 123 | 124 | /** 125 | * The merge operation provider construct which is responsible for configuring the Lambda resource that will be invoked during 126 | * Cloudformation update. 127 | */ 128 | readonly mergeOperationProvider?: ISourceApiAssociationMergeOperationProvider; 129 | 130 | /** 131 | * The version identifier for the schema merge operation. Any change to the version identifier will trigger a merge on the next 132 | * update. Use the version identifier property to control when the source API metadata is merged. 133 | * 134 | * @default null 135 | */ 136 | readonly versionIdentifier?: string; 137 | 138 | /** 139 | * Flag indicating whether the source api should be merged on every CFN update or not. 140 | * If set to true and there are no changes to the source API, this will result in a no-op merge operation. 141 | * 142 | * @default False 143 | */ 144 | readonly alwaysMergeOnStackUpdate?: boolean; 145 | } 146 | 147 | /** 148 | * Type used to define the input properties to the merge operation custom resource. 149 | */ 150 | type MergeResourceProperties = { 151 | 152 | /** 153 | * The assocition id. 154 | */ 155 | associationId: string; 156 | 157 | /** 158 | * The merged api identifier. 159 | */ 160 | mergedApiIdentifier: string; 161 | 162 | /** 163 | * The source api identifier. 164 | */ 165 | sourceApiIdentifier: string; 166 | 167 | /** 168 | * A version identifier field. Changing the version identifier will trigger a merge operation on update. 169 | */ 170 | versionIdentifier?: string; 171 | 172 | /** 173 | * Whether the resource will always update or not. When always update is enabled, we use a random UUID to trigger a 174 | * merge operation after each deployment following a synthesis of the stack. 175 | */ 176 | alwaysUpdate?: string; 177 | }; 178 | 179 | /** 180 | * The SourceApiAssociationMergeOperation triggers a merge of a source API during a Cloudformation stack update. 181 | * This can be used to propagate changes from the source API to the Merged API when the association is using type MANUAL_MERGE. 182 | * If the merge operation fails, it will fail the Cloudformation update and rollback the stack. 183 | */ 184 | export class SourceApiAssociationMergeOperation extends Construct { 185 | 186 | constructor(scope: Construct, id: string, props: SourceApiAssociationMergeOperationProps) { 187 | super(scope, id); 188 | 189 | if (!props.alwaysMergeOnStackUpdate && !props.versionIdentifier) { 190 | throw new Error('A version identifier must be specified when the alwaysMergeOnStackUpdate flag is false'); 191 | } 192 | 193 | var mergeOperationProvider = props.mergeOperationProvider; 194 | if (!mergeOperationProvider) { 195 | mergeOperationProvider = this.getOrCreateMergeOperationProvider(); 196 | } 197 | 198 | mergeOperationProvider.associateSourceApiAssociation(props.sourceApiAssociation); 199 | 200 | var properties: MergeResourceProperties = { 201 | associationId: props.sourceApiAssociation.associationId, 202 | mergedApiIdentifier: props.sourceApiAssociation.mergedApi.arn, 203 | sourceApiIdentifier: props.sourceApiAssociation.sourceApi.arn, 204 | }; 205 | 206 | // When versionIdentifier property is passed, it allows the developer to explicitly control when the source api is merged via the merge operation. 207 | // Changing the version identifier will allow you to control whether the merge operation occurs for a specific merge operation of not. 208 | if (props.versionIdentifier) { 209 | properties.versionIdentifier = props.versionIdentifier; 210 | } 211 | 212 | // When alwaysMergeOnStackUpdate flag is set to true, everytime the stack is deployed we create a new custom resource which ensures that this 213 | // merge operation is always ran. This potentially will result in no-ops. 214 | if (props.alwaysMergeOnStackUpdate) { 215 | properties.alwaysUpdate = randomUUID(); 216 | } 217 | 218 | // Custom resource for the merge of this specific source api association. 219 | const customResource = new CustomResource(this, 'SourceApiSchemaMergeOperation', { 220 | serviceToken: mergeOperationProvider.serviceToken, 221 | resourceType: 'Custom::AppSyncSourceApiMergeOperation', 222 | properties: { 223 | ...properties, 224 | }, 225 | }); 226 | 227 | // If a reference to the source API exists, 228 | // add a dependency on all children of the source api in order to ensure that this resource is created at the end. 229 | props.sourceApiAssociation.sourceApi.node.children.forEach((child) => { 230 | if (CfnResource.isCfnResource(child)) { 231 | customResource.node.addDependency(child); 232 | } 233 | 234 | if (Construct.isConstruct(child) && child.node.defaultChild && CfnResource.isCfnResource(child.node.defaultChild)) { 235 | customResource.node.addDependency(child.node.defaultChild); 236 | } 237 | }); 238 | } 239 | 240 | /** 241 | * Get an existing merge operation provider from the current stack or create a new stack scoped merge operation provider. 242 | * @returns SourceApiAssociationMergeOperationProvider 243 | */ 244 | private getOrCreateMergeOperationProvider(): SourceApiAssociationMergeOperationProvider { 245 | const constructName = 'SchemaMergeOperationProvider'; 246 | const stack = Stack.of(this); 247 | const existing = stack.node.tryFindChild(constructName); 248 | if (!existing) { 249 | return new SourceApiAssociationMergeOperationProvider(stack, 'SchemaMergeOperationProvider', { 250 | pollingInterval: Duration.seconds(30), 251 | }); 252 | } else { 253 | return existing as SourceApiAssociationMergeOperationProvider; 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /test/appsync-code-first.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { GraphqlApi } from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, Directive, Field, InterfaceType, ObjectType, ResolvableField } from '../src'; 6 | 7 | let stack: cdk.Stack; 8 | beforeEach(() => { 9 | // GIVEN 10 | stack = new cdk.Stack(); 11 | }); 12 | 13 | describe('code-first implementation through GraphQL Api functions`', () => { 14 | let api: GraphqlApi; 15 | let schema: CodeFirstSchema; 16 | beforeEach(() => { 17 | // GIVEN 18 | schema = new CodeFirstSchema(); 19 | api = new GraphqlApi(stack, 'api', { 20 | name: 'api', 21 | schema, 22 | }); 23 | }); 24 | 25 | test('testing addType w/ Interface Type for schema definition mode `code`', () => { 26 | // WHEN 27 | const test = new InterfaceType('Test', { 28 | definition: { 29 | id: t.id, 30 | lid: t.list_id, 31 | rid: t.required_id, 32 | rlid: t.required_list_id, 33 | rlrid: t.required_list_required_id, 34 | }, 35 | }); 36 | schema.addType(test); 37 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 38 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 39 | 40 | // THEN 41 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 42 | Definition: `${out}`, 43 | }); 44 | }); 45 | 46 | test('testing addType w/ Object Type for schema definition mode `code`', () => { 47 | // WHEN 48 | const test = new ObjectType('Test', { 49 | definition: { 50 | id: t.id, 51 | lid: t.list_id, 52 | rid: t.required_id, 53 | rlid: t.required_list_id, 54 | rlrid: t.required_list_required_id, 55 | }, 56 | }); 57 | schema.addType(test); 58 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 59 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 60 | 61 | // THEN 62 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 63 | Definition: `${out}`, 64 | }); 65 | }); 66 | 67 | test('testing addObjectType for schema definition mode `code`', () => { 68 | // WHEN 69 | schema.addType(new ObjectType('Test', { 70 | definition: { 71 | id: t.id, 72 | lid: t.list_id, 73 | rid: t.required_id, 74 | rlid: t.required_list_id, 75 | rlrid: t.required_list_required_id, 76 | dupid: t.dup_id, 77 | }, 78 | })); 79 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 80 | 81 | // THEN 82 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 83 | Definition: `${out}`, 84 | }); 85 | }); 86 | 87 | test('addField dynamically adds field to schema for ObjectType', () => { 88 | // WHEN 89 | const test = schema.addType(new ObjectType('Test', { 90 | definition: { 91 | id: t.id, 92 | lid: t.list_id, 93 | rid: t.required_id, 94 | rlid: t.required_list_id, 95 | rlrid: t.required_list_required_id, 96 | }, 97 | })); 98 | 99 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 100 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 101 | 102 | // THEN 103 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 104 | Definition: `${out}`, 105 | }); 106 | }); 107 | 108 | test('testing addInterfaceType for schema definition mode `code`', () => { 109 | // WHEN 110 | schema.addType(new InterfaceType('Test', { 111 | definition: { 112 | id: t.id, 113 | lid: t.list_id, 114 | rid: t.required_id, 115 | rlid: t.required_list_id, 116 | rlrid: t.required_list_required_id, 117 | dupid: t.dup_id, 118 | }, 119 | })); 120 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 121 | 122 | // THEN 123 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 124 | Definition: `${out}`, 125 | }); 126 | }); 127 | 128 | test('addField dynamically adds field to schema for InterfaceType', () => { 129 | // WHEN 130 | const test = schema.addType(new InterfaceType('Test', { 131 | definition: { 132 | id: t.id, 133 | lid: t.list_id, 134 | rid: t.required_id, 135 | rlid: t.required_list_id, 136 | rlrid: t.required_list_required_id, 137 | }, 138 | })); 139 | 140 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 141 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 142 | 143 | // THEN 144 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 145 | Definition: `${out}`, 146 | }); 147 | }); 148 | 149 | test('addSubscription allows for adding fields but not resolvable fields', () => { 150 | const ds = api.addNoneDataSource('DS'); 151 | 152 | // WHEN 153 | schema.addMutation('addId', new ResolvableField({ 154 | returnType: t.required_id, 155 | args: { id: t.required_id }, 156 | dataSource: ds, 157 | })); 158 | schema.addSubscription('addedId', new Field({ 159 | returnType: t.required_id, 160 | args: { id: t.required_id }, 161 | directives: [Directive.subscribe('addId')], 162 | })); 163 | 164 | const schemaDef = 'schema {\n mutation: Mutation\n subscription: Subscription\n}\n'; 165 | const mutationDef = 'type Mutation {\n addId(id: ID!): ID!\n}\n'; 166 | const subscriptionDef = 'type Subscription {\n addedId(id: ID!): ID!\n @aws_subscribe(mutations: ["addId"])\n}\n'; 167 | 168 | // THEN 169 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 170 | Definition: `${schemaDef}${mutationDef}${subscriptionDef}`, 171 | }); 172 | }); 173 | }); 174 | 175 | describe('code-first implementation through Schema functions`', () => { 176 | let schema: CodeFirstSchema; 177 | beforeEach(() => { 178 | // GIVEN 179 | schema = new CodeFirstSchema(); 180 | }); 181 | 182 | test('testing addType w/ Interface Type for schema definition mode `code`', () => { 183 | // WHEN 184 | const test = new InterfaceType('Test', { 185 | definition: { 186 | id: t.id, 187 | lid: t.list_id, 188 | rid: t.required_id, 189 | rlid: t.required_list_id, 190 | rlrid: t.required_list_required_id, 191 | }, 192 | }); 193 | schema.addType(test); 194 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 195 | 196 | new GraphqlApi(stack, 'api', { 197 | name: 'api', 198 | schema, 199 | }); 200 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 201 | 202 | // THEN 203 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 204 | Definition: `${out}`, 205 | }); 206 | }); 207 | 208 | test('testing addType w/ Object Type for schema definition mode `code`', () => { 209 | // WHEN 210 | const test = new ObjectType('Test', { 211 | definition: { 212 | id: t.id, 213 | lid: t.list_id, 214 | rid: t.required_id, 215 | rlid: t.required_list_id, 216 | rlrid: t.required_list_required_id, 217 | }, 218 | }); 219 | schema.addType(test); 220 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 221 | 222 | new GraphqlApi(stack, 'api', { 223 | name: 'api', 224 | schema, 225 | }); 226 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 227 | 228 | // THEN 229 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 230 | Definition: `${out}`, 231 | }); 232 | }); 233 | 234 | test('testing addObjectType for schema definition mode `code`', () => { 235 | // WHEN 236 | schema.addType(new ObjectType('Test', { 237 | definition: { 238 | id: t.id, 239 | lid: t.list_id, 240 | rid: t.required_id, 241 | rlid: t.required_list_id, 242 | rlrid: t.required_list_required_id, 243 | dupid: t.dup_id, 244 | }, 245 | })); 246 | 247 | new GraphqlApi(stack, 'api', { 248 | name: 'api', 249 | schema, 250 | }); 251 | 252 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 253 | 254 | // THEN 255 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 256 | Definition: `${out}`, 257 | }); 258 | }); 259 | 260 | test('schema.addField dynamically adds field to schema for ObjectType', () => { 261 | // WHEN 262 | const test = schema.addType(new ObjectType('Test', { 263 | definition: { 264 | id: t.id, 265 | lid: t.list_id, 266 | rid: t.required_id, 267 | rlid: t.required_list_id, 268 | rlrid: t.required_list_required_id, 269 | }, 270 | })); 271 | 272 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 273 | new GraphqlApi(stack, 'api', { 274 | name: 'api', 275 | schema, 276 | }); 277 | const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 278 | 279 | // THEN 280 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 281 | Definition: `${out}`, 282 | }); 283 | }); 284 | 285 | test('testing addInterfaceType for schema definition mode `code`', () => { 286 | // WHEN 287 | schema.addType(new InterfaceType('Test', { 288 | definition: { 289 | id: t.id, 290 | lid: t.list_id, 291 | rid: t.required_id, 292 | rlid: t.required_list_id, 293 | rlrid: t.required_list_required_id, 294 | dupid: t.dup_id, 295 | }, 296 | })); 297 | new GraphqlApi(stack, 'api', { 298 | name: 'api', 299 | schema, 300 | }); 301 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 302 | 303 | // THEN 304 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 305 | Definition: `${out}`, 306 | }); 307 | }); 308 | 309 | test('schema addField dynamically adds field to schema for InterfaceType', () => { 310 | // WHEN 311 | const test = schema.addType(new InterfaceType('Test', { 312 | definition: { 313 | id: t.id, 314 | lid: t.list_id, 315 | rid: t.required_id, 316 | rlid: t.required_list_id, 317 | rlrid: t.required_list_required_id, 318 | }, 319 | })); 320 | 321 | test.addField({ fieldName: 'dupid', field: t.dup_id }); 322 | new GraphqlApi(stack, 'api', { 323 | name: 'api', 324 | schema, 325 | }); 326 | const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; 327 | 328 | // THEN 329 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 330 | Definition: `${out}`, 331 | }); 332 | }); 333 | }); 334 | -------------------------------------------------------------------------------- /test/appsync-directives.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 5 | import * as t from './scalar-type-defintions'; 6 | import { CodeFirstSchema, Directive, Field, InterfaceType, ObjectType, ResolvableField } from '../src'; 7 | 8 | const generateField = (directives: Directive[]): Field => { 9 | return new Field({ 10 | returnType: t.string, 11 | directives, 12 | }); 13 | }; 14 | 15 | const generateRField = (directives: Directive[]): ResolvableField => { 16 | return new ResolvableField({ 17 | returnType: t.string, 18 | directives, 19 | }); 20 | }; 21 | 22 | interface AuthType { 23 | stack: cdk.Stack; 24 | directive: Directive; 25 | tag: string; 26 | authorizationConfig?: appsync.AuthorizationConfig; 27 | }; 28 | 29 | /** 30 | * List of all built in auth type directives 31 | * 32 | * A list of all test cases to test correct insertion of all built in auth type directives. 33 | */ 34 | const makeAuthTypes = (): AuthType[] => { 35 | const cognitoDefaultStack = new cdk.Stack(); 36 | const cognitoAddlStack = new cdk.Stack(); 37 | return [{ 38 | stack: new cdk.Stack(), 39 | directive: Directive.iam(), 40 | tag: '@aws_iam', 41 | authorizationConfig: { 42 | defaultAuthorization: { 43 | authorizationType: appsync.AuthorizationType.IAM, 44 | }, 45 | }, 46 | }, { 47 | stack: new cdk.Stack(), 48 | directive: Directive.oidc(), 49 | tag: '@aws_oidc', 50 | authorizationConfig: { 51 | defaultAuthorization: { 52 | authorizationType: appsync.AuthorizationType.OIDC, 53 | openIdConnectConfig: { oidcProvider: 'test' }, 54 | }, 55 | }, 56 | }, { 57 | stack: new cdk.Stack(), 58 | directive: Directive.apiKey(), 59 | tag: '@aws_api_key', 60 | }, { 61 | stack: cognitoDefaultStack, 62 | directive: Directive.cognito('test', 'test2'), 63 | tag: '@aws_auth(cognito_groups: ["test", "test2"])', 64 | authorizationConfig: { 65 | defaultAuthorization: { 66 | authorizationType: appsync.AuthorizationType.USER_POOL, 67 | userPoolConfig: { userPool: new cognito.UserPool(cognitoDefaultStack, 'userpool') }, 68 | }, 69 | }, 70 | }, { 71 | stack: cognitoAddlStack, 72 | directive: Directive.cognito('test', 'test2'), 73 | tag: '@aws_cognito_user_pools(cognito_groups: ["test", "test2"])', 74 | authorizationConfig: { 75 | additionalAuthorizationModes: [ 76 | { 77 | authorizationType: appsync.AuthorizationType.USER_POOL, 78 | userPoolConfig: { userPool: new cognito.UserPool(cognitoAddlStack, 'userpool') }, 79 | }, 80 | ], 81 | }, 82 | }, { 83 | stack: new cdk.Stack(), 84 | directive: Directive.custom('custom'), 85 | tag: 'custom', 86 | }]; 87 | }; 88 | 89 | describe('Basic Testing of Directives for Code-First', () => { 90 | test.each(makeAuthTypes())('$tag directive configures in Object Type', ({ directive, tag, authorizationConfig, stack }) => { 91 | // WHEN 92 | const schema = new CodeFirstSchema(); 93 | new appsync.GraphqlApi(stack, 'api_iam', { 94 | name: 'api', 95 | authorizationConfig, 96 | schema, 97 | }); 98 | 99 | schema.addType(new ObjectType('Test', { 100 | definition: { 101 | field: generateField([directive]), 102 | rfield: generateRField([directive]), 103 | }, 104 | directives: [directive], 105 | })); 106 | // THEN 107 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 108 | Definition: `type Test ${tag} {\n field: String\n ${tag}\n rfield: String\n ${tag}\n}\n`, 109 | }); 110 | }); 111 | 112 | test.each(makeAuthTypes())('$tag directive configures in Interface Type', ({ directive, tag, authorizationConfig, stack }) => { 113 | // WHEN 114 | const schema = new CodeFirstSchema(); 115 | new appsync.GraphqlApi(stack, 'api_iam', { 116 | name: 'api', 117 | authorizationConfig, 118 | schema, 119 | }); 120 | 121 | schema.addType(new InterfaceType('Test', { 122 | definition: { 123 | field: generateField([directive]), 124 | rfield: generateRField([directive], 125 | ), 126 | }, 127 | directives: [directive], 128 | })); 129 | // THEN 130 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 131 | Definition: `interface Test ${tag} {\n field: String\n ${tag}\n rfield: String\n ${tag}\n}\n`, 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/appsync-enum-type.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, EnumType, ObjectType } from '../src'; 6 | 7 | const out = 'enum Test {\n test1\n test2\n test3\n}\n'; 8 | let stack: cdk.Stack; 9 | let schema: CodeFirstSchema; 10 | beforeEach(() => { 11 | // GIVEN 12 | stack = new cdk.Stack(); 13 | schema = new CodeFirstSchema(); 14 | new appsync.GraphqlApi(stack, 'api', { 15 | name: 'api', 16 | schema, 17 | }); 18 | }); 19 | 20 | describe('testing Enum Type properties', () => { 21 | test('EnumType configures properly', () => { 22 | // WHEN 23 | const test = new EnumType('Test', { 24 | definition: ['test1', 'test2', 'test3'], 25 | }); 26 | schema.addType(test); 27 | 28 | // THEN 29 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 30 | Definition: `${out}`, 31 | }); 32 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 0); 33 | }); 34 | 35 | test('EnumType can addField', () => { 36 | // WHEN 37 | const test = new EnumType('Test', { 38 | definition: ['test1', 'test2'], 39 | }); 40 | schema.addType(test); 41 | test.addField({ fieldName: 'test3' }); 42 | 43 | // THEN 44 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 45 | Definition: `${out}`, 46 | }); 47 | }); 48 | 49 | test('EnumType can be a GraphqlType', () => { 50 | // WHEN 51 | const test = new EnumType('Test', { 52 | definition: ['test1', 'test2', 'test3'], 53 | }); 54 | schema.addType(test); 55 | 56 | schema.addType(new ObjectType('Test2', { 57 | definition: { enum: test.attribute() }, 58 | })); 59 | 60 | const obj = 'type Test2 {\n enum: Test\n}\n'; 61 | 62 | // THEN 63 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 64 | Definition: `${out}${obj}`, 65 | }); 66 | }); 67 | 68 | test('errors when enum type is configured with white space', () => { 69 | // THEN 70 | expect(() => { 71 | new EnumType('Test', { 72 | definition: ['test 1', 'test2', 'test3'], 73 | }); 74 | }).toThrowError('Enum Type values cannot have whitespace. Received: test 1'); 75 | }); 76 | 77 | test('errors when the fieldName in addField has white space', () => { 78 | // WHEN 79 | const test = new EnumType('Test', { 80 | definition: [], 81 | }); 82 | // THEN 83 | expect(() => { 84 | test.addField({ fieldName: ' ' }); 85 | }).toThrowError('Enum Type values cannot have whitespace. Received: '); 86 | }); 87 | 88 | test('errors when enum type is configured with field options', () => { 89 | // WHEN 90 | const test = new EnumType('Test', { 91 | definition: [], 92 | }); 93 | // THEN 94 | expect(() => { 95 | test.addField({ fieldName: 'test', field: t.string }); 96 | }).toThrowError('Enum Type fields consist of strings. Use the fieldName option instead of the field option.'); 97 | }); 98 | 99 | test('errors when enum type is missing fieldName option', () => { 100 | // WHEN 101 | const test = new EnumType('Test', { 102 | definition: [], 103 | }); 104 | // THEN 105 | expect(() => { 106 | test.addField({}); 107 | }).toThrowError('When adding a field to an Enum Type, you must configure the fieldName option.'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/appsync-input-types.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, InputType, ObjectType } from '../src'; 6 | 7 | const out = 'input Test {\n test: String\n}\n'; 8 | let stack: cdk.Stack; 9 | let schema: CodeFirstSchema; 10 | beforeEach(() => { 11 | // GIVEN 12 | stack = new cdk.Stack(); 13 | schema = new CodeFirstSchema(); 14 | new appsync.GraphqlApi(stack, 'api', { 15 | name: 'api', 16 | schema, 17 | }); 18 | }); 19 | 20 | describe('testing Input Type properties', () => { 21 | test('InputType configures properly', () => { 22 | // WHEN 23 | const test = new InputType('Test', { 24 | definition: { test: t.string }, 25 | }); 26 | schema.addType(test); 27 | 28 | // THEN 29 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 30 | Definition: `${out}`, 31 | }); 32 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 0); 33 | }); 34 | 35 | test('InputType can addField', () => { 36 | // WHEN 37 | const test = new InputType('Test', { definition: {} }); 38 | schema.addType(test); 39 | test.addField({ fieldName: 'test', field: t.string }); 40 | 41 | // THEN 42 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 43 | Definition: `${out}`, 44 | }); 45 | }); 46 | 47 | test('appsync fails addField with InputType missing fieldName', () => { 48 | // WHEN 49 | const test = new InputType('Test', { definition: {} }); 50 | schema.addType(test); 51 | 52 | // THEN 53 | expect(() => { 54 | test.addField({ fieldName: 'test' }); 55 | }).toThrowError('Input Types must have both fieldName and field options.'); 56 | }); 57 | 58 | test('appsync fails addField with InputType missing field', () => { 59 | // WHEN 60 | const test = new InputType('Test', { definition: {} }); 61 | schema.addType(test); 62 | 63 | // THEN 64 | expect(() => { 65 | test.addField({ field: t.string }); 66 | }).toThrowError('Input Types must have both fieldName and field options.'); 67 | }); 68 | 69 | test('appsync fails addField with InputType missing both fieldName and field options', () => { 70 | // WHEN 71 | const test = new InputType('Test', { definition: {} }); 72 | schema.addType(test); 73 | 74 | // THEN 75 | expect(() => { 76 | test.addField({}); 77 | }).toThrowError('Input Types must have both fieldName and field options.'); 78 | }); 79 | 80 | test('InputType can be a GraphqlType', () => { 81 | // WHEN 82 | const test = new InputType('Test', { 83 | definition: { test: t.string }, 84 | }); 85 | schema.addType(test); 86 | 87 | schema.addType(new ObjectType('Test2', { 88 | definition: { input: test.attribute() }, 89 | })); 90 | 91 | const obj = 'type Test2 {\n input: Test\n}\n'; 92 | 93 | // THEN 94 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 95 | Definition: `${out}${obj}`, 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/appsync-interface-type.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, Directive, Field, InterfaceType, ObjectType, ResolvableField } from '../src'; 6 | 7 | let stack: cdk.Stack; 8 | let schema: CodeFirstSchema; 9 | let api: appsync.GraphqlApi; 10 | beforeEach(() => { 11 | // GIVEN 12 | stack = new cdk.Stack(); 13 | schema = new CodeFirstSchema(); 14 | api = new appsync.GraphqlApi(stack, 'api', { 15 | name: 'api', 16 | schema, 17 | }); 18 | }); 19 | 20 | describe('testing InterfaceType properties', () => { 21 | let baseTest: InterfaceType; 22 | beforeEach(()=>{ 23 | baseTest = new InterfaceType('baseTest', { 24 | definition: { 25 | id: t.id, 26 | }, 27 | }); 28 | }); 29 | test('basic InterfaceType produces correct schema', () => { 30 | // WHEN 31 | schema.addToSchema(baseTest.toString()); 32 | const out = 'interface baseTest {\n id: ID\n}\n'; 33 | 34 | // THEN 35 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 36 | Definition: `${out}`, 37 | }); 38 | }); 39 | 40 | test('InterfaceType fields can have arguments', () => { 41 | // WHEN 42 | baseTest.addField({ 43 | fieldName: 'test', 44 | field: new Field({ 45 | returnType: t.string, 46 | args: { success: t.int }, 47 | }), 48 | }); 49 | schema.addToSchema(baseTest.toString()); 50 | const out = 'interface baseTest {\n id: ID\n test(success: Int): String\n}\n'; 51 | 52 | // THEN 53 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 54 | Definition: `${out}`, 55 | }); 56 | }); 57 | 58 | test('InterfaceType fields will not produce resolvers', () => { 59 | // WHEN 60 | baseTest.addField({ 61 | fieldName: 'test', 62 | field: new ResolvableField({ 63 | returnType: t.string, 64 | args: { success: t.int }, 65 | dataSource: api.addNoneDataSource('none'), 66 | }), 67 | }); 68 | schema.addToSchema(baseTest.toString()); 69 | const out = 'interface baseTest {\n id: ID\n test(success: Int): String\n}\n'; 70 | 71 | // THEN 72 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 73 | Definition: `${out}`, 74 | }); 75 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 0); 76 | }); 77 | 78 | test('Interface Type can be a Graphql Type', () => { 79 | // WHEN 80 | const graphqlType = baseTest.attribute(); 81 | 82 | const test = new ObjectType('Test', { 83 | definition: { 84 | test: graphqlType, 85 | }, 86 | }); 87 | schema.addToSchema(test.toString()); 88 | const out = 'type Test {\n test: baseTest\n}\n'; 89 | 90 | // THEN 91 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 92 | Definition: `${out}`, 93 | }); 94 | }); 95 | 96 | test('Interface Type can generate Fields with Directives', () => { 97 | // WHEN 98 | const test = new InterfaceType('Test', { 99 | definition: { 100 | test: t.string, 101 | }, 102 | }); 103 | test.addField({ 104 | fieldName: 'resolve', 105 | field: new Field({ 106 | returnType: t.string, 107 | directives: [Directive.apiKey()], 108 | }), 109 | }); 110 | 111 | schema.addType(test); 112 | const out = 'interface Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; 113 | 114 | // THEN 115 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 116 | Definition: `${out}`, 117 | }); 118 | }); 119 | 120 | test('Interface Type can generate ResolvableFields with Directives, but not the resolver', () => { 121 | // WHEN 122 | const test = new InterfaceType('Test', { 123 | definition: { 124 | test: t.string, 125 | }, 126 | }); 127 | test.addField({ 128 | fieldName: 'resolve', 129 | field: new ResolvableField({ 130 | returnType: t.string, 131 | directives: [Directive.apiKey()], 132 | dataSource: api.addNoneDataSource('none'), 133 | }), 134 | }); 135 | 136 | schema.addType(test); 137 | const out = 'interface Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; 138 | 139 | // THEN 140 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 141 | Definition: `${out}`, 142 | }); 143 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 0); 144 | }); 145 | 146 | test('appsync fails addField with InterfaceType missing fieldName', () => { 147 | // WHEN 148 | const test = new InterfaceType('Test', { definition: {} }); 149 | schema.addType(test); 150 | 151 | // THEN 152 | expect(() => { 153 | test.addField({ fieldName: 'test' }); 154 | }).toThrowError('Interface Types must have both fieldName and field options.'); 155 | }); 156 | 157 | test('appsync fails addField with InterfaceType missing field', () => { 158 | // WHEN 159 | const test = new InterfaceType('Test', { definition: {} }); 160 | schema.addType(test); 161 | 162 | // THEN 163 | expect(() => { 164 | test.addField({ field: t.string }); 165 | }).toThrowError('Interface Types must have both fieldName and field options.'); 166 | }); 167 | 168 | test('appsync fails addField with InterfaceType missing both fieldName and field options', () => { 169 | // WHEN 170 | const test = new InterfaceType('Test', { definition: {} }); 171 | schema.addType(test); 172 | 173 | // THEN 174 | expect(() => { 175 | test.addField({}); 176 | }).toThrowError('Interface Types must have both fieldName and field options.'); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/appsync-object-type.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, Directive, Field, InterfaceType, ObjectType, ResolvableField } from '../src'; 6 | 7 | let stack: cdk.Stack; 8 | let api: appsync.GraphqlApi; 9 | let schema: CodeFirstSchema; 10 | beforeEach(() => { 11 | // GIVEN 12 | stack = new cdk.Stack(); 13 | schema = new CodeFirstSchema(); 14 | api = new appsync.GraphqlApi(stack, 'api', { 15 | name: 'api', 16 | schema, 17 | }); 18 | }); 19 | 20 | describe('testing Object Type properties', () => { 21 | test('ObjectType can implement from interface types', () => { 22 | // WHEN 23 | const baseTest = new InterfaceType('baseTest', { 24 | definition: { 25 | id: t.id, 26 | }, 27 | }); 28 | const objectTest = new ObjectType('objectTest', { 29 | interfaceTypes: [baseTest], 30 | definition: { 31 | id2: t.id, 32 | }, 33 | directives: [Directive.custom('@test')], 34 | }); 35 | 36 | schema.addType(baseTest); 37 | schema.addType(objectTest); 38 | const gql_interface = 'interface baseTest {\n id: ID\n}\n'; 39 | const gql_object = 'type objectTest implements baseTest @test {\n id2: ID\n id: ID\n}\n'; 40 | const out = `${gql_interface}${gql_object}`; 41 | 42 | // THEN 43 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 44 | Definition: `${out}`, 45 | }); 46 | }); 47 | 48 | test('ObjectType can implement from multiple interface types', () => { 49 | // WHEN 50 | const baseTest = new InterfaceType('baseTest', { 51 | definition: { id: t.id }, 52 | }); 53 | const anotherTest = new InterfaceType('anotherTest', { 54 | definition: { id2: t.id }, 55 | }); 56 | const objectTest = new ObjectType('objectTest', { 57 | interfaceTypes: [anotherTest, baseTest], 58 | definition: { 59 | id3: t.id, 60 | }, 61 | }); 62 | 63 | schema.addType(baseTest); 64 | schema.addType(anotherTest); 65 | schema.addType(objectTest); 66 | 67 | const gql_interface = 'interface baseTest {\n id: ID\n}\ninterface anotherTest {\n id2: ID\n}\n'; 68 | const gql_object = 'type objectTest implements anotherTest & baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; 69 | const out = `${gql_interface}${gql_object}`; 70 | 71 | // THEN 72 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 73 | Definition: `${out}`, 74 | }); 75 | }); 76 | 77 | test('Object Type can be a Graphql Type', () => { 78 | // WHEN 79 | const baseTest = new ObjectType('baseTest', { 80 | definition: { 81 | id: t.id, 82 | }, 83 | }); 84 | const graphqlType = baseTest.attribute(); 85 | const test = new ObjectType('Test', { 86 | definition: { 87 | test: graphqlType, 88 | }, 89 | }); 90 | schema.addType(test); 91 | const out = 'type Test {\n test: baseTest\n}\n'; 92 | 93 | // THEN 94 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 95 | Definition: `${out}`, 96 | }); 97 | }); 98 | 99 | test('Object Type can implement Resolvable Field in definition', () => { 100 | // WHEN 101 | const field = new ResolvableField({ 102 | returnType: t.string, 103 | dataSource: api.addNoneDataSource('none'), 104 | args: { 105 | arg: t.int, 106 | }, 107 | 108 | }); 109 | const test = new ObjectType('Test', { 110 | definition: { 111 | test: t.string, 112 | resolve: field, 113 | }, 114 | }); 115 | schema.addType(test); 116 | const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; 117 | 118 | // THEN 119 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 120 | Definition: `${out}`, 121 | }); 122 | }); 123 | 124 | test('Object Type can implement Resolvable Field from GraphqlType', () => { 125 | // WHEN 126 | const field = new ResolvableField({ 127 | returnType: t.string, 128 | dataSource: api.addNoneDataSource('none'), 129 | args: { 130 | arg: t.int, 131 | }, 132 | 133 | }); 134 | const test = new ObjectType('Test', { 135 | definition: { 136 | test: t.string, 137 | resolve: field, 138 | }, 139 | }); 140 | schema.addType(test); 141 | const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; 142 | 143 | // THEN 144 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 145 | Definition: `${out}`, 146 | }); 147 | }); 148 | 149 | test('Object Type can implement Resolvable Field for pipelineResolvers', () => { 150 | // WHEN 151 | const ds = api.addNoneDataSource('none'); 152 | const test1 = ds.createFunction('Test1Function', { 153 | name: 'test1', 154 | }); 155 | const test2 = ds.createFunction('Test2Function', { 156 | name: 'test2', 157 | }); 158 | const test = new ObjectType('Test', { 159 | definition: { 160 | resolve: new ResolvableField({ 161 | returnType: t.string, 162 | args: { 163 | arg: t.int, 164 | }, 165 | pipelineConfig: [test1, test2], 166 | requestMappingTemplate: appsync.MappingTemplate.fromString(JSON.stringify({ 167 | version: '2017-02-28', 168 | })), 169 | responseMappingTemplate: appsync.MappingTemplate.fromString(JSON.stringify({ 170 | version: 'v1', 171 | })), 172 | }), 173 | }, 174 | }); 175 | schema.addType(test); 176 | 177 | // THEN 178 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::Resolver', { 179 | Kind: 'PIPELINE', 180 | PipelineConfig: { 181 | Functions: [ 182 | { 'Fn::GetAtt': ['apiTest1Function793605E9', 'FunctionId'] }, 183 | { 'Fn::GetAtt': ['apiTest2FunctionB704A7AD', 'FunctionId'] }, 184 | ], 185 | }, 186 | }); 187 | }); 188 | 189 | test('Object Type can dynamically add Fields', () => { 190 | // WHEN 191 | const field = new ResolvableField({ 192 | returnType: t.string, 193 | dataSource: api.addNoneDataSource('none'), 194 | args: { arg: t.int }, 195 | 196 | }); 197 | const test = new ObjectType('Test', { 198 | definition: { 199 | test: t.string, 200 | }, 201 | }); 202 | test.addField({ fieldName: 'resolve', field }); 203 | test.addField({ fieldName: 'dynamic', field: t.string }); 204 | 205 | schema.addType(test); 206 | const out = 'type Test {\n test: String\n resolve(arg: Int): String\n dynamic: String\n}\n'; 207 | 208 | // THEN 209 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 210 | Definition: `${out}`, 211 | }); 212 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 1); 213 | }); 214 | 215 | test('Object Type can generate Fields with Directives', () => { 216 | // WHEN 217 | const test = new ObjectType('Test', { 218 | definition: { 219 | test: t.string, 220 | }, 221 | }); 222 | test.addField({ 223 | fieldName: 'resolve', 224 | field: new Field({ 225 | returnType: t.string, 226 | directives: [Directive.apiKey()], 227 | }), 228 | }); 229 | 230 | schema.addType(test); 231 | const out = 'type Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; 232 | 233 | // THEN 234 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 235 | Definition: `${out}`, 236 | }); 237 | }); 238 | 239 | test('Object Type can generate ResolvableFields with Directives', () => { 240 | // WHEN 241 | const test = new ObjectType('Test', { 242 | definition: { 243 | test: t.string, 244 | }, 245 | }); 246 | const field = new ResolvableField({ 247 | returnType: t.string, 248 | directives: [Directive.apiKey()], 249 | dataSource: api.addNoneDataSource('none'), 250 | args: { 251 | arg: t.string, 252 | }, 253 | 254 | }); 255 | test.addField({ fieldName: 'resolve', field }); 256 | 257 | schema.addType(test); 258 | const out = 'type Test {\n test: String\n resolve(arg: String): String\n @aws_api_key\n}\n'; 259 | 260 | // THEN 261 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 262 | Definition: `${out}`, 263 | }); 264 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 1); 265 | }); 266 | 267 | test('appsync fails addField with ObjectType missing fieldName', () => { 268 | // WHEN 269 | const test = new ObjectType('Test', { definition: {} }); 270 | schema.addType(test); 271 | 272 | // THEN 273 | expect(() => { 274 | test.addField({ fieldName: 'test' }); 275 | }).toThrowError('Object Types must have both fieldName and field options.'); 276 | }); 277 | 278 | test('appsync fails addField with ObjectType missing field', () => { 279 | // WHEN 280 | const test = new ObjectType('Test', { definition: {} }); 281 | schema.addType(test); 282 | 283 | // THEN 284 | expect(() => { 285 | test.addField({ field: t.string }); 286 | }).toThrowError('Object Types must have both fieldName and field options.'); 287 | }); 288 | 289 | test('appsync fails addField with ObjectType missing both fieldName and field options', () => { 290 | // WHEN 291 | const test = new ObjectType('Test', { definition: {} }); 292 | schema.addType(test); 293 | 294 | // THEN 295 | expect(() => { 296 | test.addField({}); 297 | }).toThrowError('Object Types must have both fieldName and field options.'); 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /test/appsync-scalar-type.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, ObjectType } from '../src'; 6 | 7 | let stack: cdk.Stack; 8 | let schema: CodeFirstSchema; 9 | beforeEach(() => { 10 | // GIVEN 11 | stack = new cdk.Stack(); 12 | schema = new CodeFirstSchema(); 13 | new appsync.GraphqlApi(stack, 'api', { 14 | name: 'api', 15 | schema, 16 | }); 17 | }); 18 | 19 | describe('testing all GraphQL Types', () => { 20 | test('scalar type id', () => { 21 | // WHEN 22 | schema.addType(new ObjectType('Test', { 23 | definition: { 24 | id: t.id, 25 | }, 26 | })); 27 | const out = 'type Test {\n id: ID\n}\n'; 28 | 29 | // THEN 30 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 31 | Definition: `${out}`, 32 | }); 33 | }); 34 | 35 | test('scalar type string', () => { 36 | // WHEN 37 | schema.addType(new ObjectType('Test', { 38 | definition: { 39 | id: t.string, 40 | }, 41 | })); 42 | const out = 'type Test {\n id: String\n}\n'; 43 | 44 | // THEN 45 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 46 | Definition: `${out}`, 47 | }); 48 | }); 49 | 50 | test('scalar type int', () => { 51 | // WHEN 52 | schema.addType(new ObjectType('Test', { 53 | definition: { 54 | id: t.int, 55 | }, 56 | })); 57 | const out = 'type Test {\n id: Int\n}\n'; 58 | 59 | // THEN 60 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 61 | Definition: `${out}`, 62 | }); 63 | }); 64 | 65 | test('scalar type float', () => { 66 | // WHEN 67 | schema.addType(new ObjectType('Test', { 68 | definition: { 69 | id: t.float, 70 | }, 71 | })); 72 | const out = 'type Test {\n id: Float\n}\n'; 73 | 74 | // THEN 75 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 76 | Definition: `${out}`, 77 | }); 78 | }); 79 | 80 | test('scalar type boolean', () => { 81 | // WHEN 82 | schema.addType(new ObjectType('Test', { 83 | definition: { 84 | id: t.boolean, 85 | }, 86 | })); 87 | const out = 'type Test {\n id: Boolean\n}\n'; 88 | 89 | // THEN 90 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 91 | Definition: `${out}`, 92 | }); 93 | }); 94 | 95 | test('scalar type AWSDate', () => { 96 | // WHEN 97 | schema.addType(new ObjectType('Test', { 98 | definition: { 99 | id: t.awsDate, 100 | }, 101 | })); 102 | const out = 'type Test {\n id: AWSDate\n}\n'; 103 | 104 | // THEN 105 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 106 | Definition: `${out}`, 107 | }); 108 | }); 109 | 110 | test('scalar type AWSTime', () => { 111 | // WHEN 112 | schema.addType(new ObjectType('Test', { 113 | definition: { 114 | id: t.awsTime, 115 | }, 116 | })); 117 | const out = 'type Test {\n id: AWSTime\n}\n'; 118 | 119 | // THEN 120 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 121 | Definition: `${out}`, 122 | }); 123 | }); 124 | 125 | test('scalar type AWSDateTime', () => { 126 | // WHEN 127 | schema.addType(new ObjectType('Test', { 128 | definition: { 129 | id: t.awsDateTime, 130 | }, 131 | })); 132 | const out = 'type Test {\n id: AWSDateTime\n}\n'; 133 | 134 | // THEN 135 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 136 | Definition: `${out}`, 137 | }); 138 | }); 139 | 140 | test('scalar type AWSTimestamp', () => { 141 | // WHEN 142 | schema.addType(new ObjectType('Test', { 143 | definition: { 144 | id: t.awsTimestamp, 145 | }, 146 | })); 147 | const out = 'type Test {\n id: AWSTimestamp\n}\n'; 148 | 149 | // THEN 150 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 151 | Definition: `${out}`, 152 | }); 153 | }); 154 | 155 | test('scalar type AWSEmail', () => { 156 | // WHEN 157 | schema.addType(new ObjectType('Test', { 158 | definition: { 159 | id: t.awsEmail, 160 | }, 161 | })); 162 | const out = 'type Test {\n id: AWSEmail\n}\n'; 163 | 164 | // THEN 165 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 166 | Definition: `${out}`, 167 | }); 168 | }); 169 | 170 | test('scalar type AWSJSON', () => { 171 | // WHEN 172 | schema.addType(new ObjectType('Test', { 173 | definition: { 174 | id: t.awsJson, 175 | }, 176 | })); 177 | const out = 'type Test {\n id: AWSJSON\n}\n'; 178 | 179 | // THEN 180 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 181 | Definition: `${out}`, 182 | }); 183 | }); 184 | 185 | 186 | test('scalar type AWSUrl', () => { 187 | // WHEN 188 | schema.addType(new ObjectType('Test', { 189 | definition: { 190 | id: t.awsUrl, 191 | }, 192 | })); 193 | const out = 'type Test {\n id: AWSURL\n}\n'; 194 | 195 | // THEN 196 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 197 | Definition: `${out}`, 198 | }); 199 | }); 200 | 201 | test('scalar type AWSPhone', () => { 202 | // WHEN 203 | schema.addType(new ObjectType('Test', { 204 | definition: { 205 | id: t.awsPhone, 206 | }, 207 | })); 208 | const out = 'type Test {\n id: AWSPhone\n}\n'; 209 | 210 | // THEN 211 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 212 | Definition: `${out}`, 213 | }); 214 | }); 215 | 216 | test('scalar type AWSIPAddress', () => { 217 | // WHEN 218 | schema.addType( new ObjectType('Test', { 219 | definition: { 220 | id: t.awsIpAddress, 221 | }, 222 | })); 223 | const out = 'type Test {\n id: AWSIPAddress\n}\n'; 224 | 225 | // THEN 226 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 227 | Definition: `${out}`, 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/appsync-schema.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, ObjectType, ResolvableField } from '../src'; 6 | 7 | // Schema Definitions 8 | const type = new ObjectType('test', { 9 | definition: { 10 | version: t.required_string, 11 | }, 12 | }); 13 | const query = new ObjectType('Query', { 14 | definition: { 15 | getTests: new ResolvableField({ 16 | returnType: type.attribute({ isRequiredList: true, isList: true }), 17 | }), 18 | }, 19 | }); 20 | const mutation = new ObjectType('Mutation', { 21 | definition: { 22 | addTest: new ResolvableField({ 23 | returnType: type.attribute(), 24 | args: { version: t.required_string }, 25 | }), 26 | }, 27 | }); 28 | 29 | let stack: cdk.Stack; 30 | beforeEach(() => { 31 | // GIVEN 32 | stack = new cdk.Stack(); 33 | }); 34 | 35 | describe('basic testing schema definition mode `code`', () => { 36 | 37 | test('definition mode `code` produces empty schema definition', () => { 38 | // WHEN 39 | new appsync.GraphqlApi(stack, 'API', { 40 | name: 'demo', 41 | schema: new CodeFirstSchema(), 42 | }); 43 | 44 | // THEN 45 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 46 | Definition: '', 47 | }); 48 | }); 49 | 50 | test('definition mode `code` generates correct schema with addToSchema', () => { 51 | // WHEN 52 | const schema = new CodeFirstSchema(); 53 | new appsync.GraphqlApi(stack, 'API', { 54 | name: 'demo', 55 | schema, 56 | }); 57 | schema.addType(type); 58 | schema.addType(query); 59 | schema.addType(mutation); 60 | 61 | // THEN 62 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 63 | Definition: `${type.toString()}\n${query.toString()}\n${mutation.toString()}\n`, 64 | }); 65 | }); 66 | 67 | test('definition mode `code` allows for api to addQuery', () => { 68 | // WHEN 69 | const schema = new CodeFirstSchema(); 70 | new appsync.GraphqlApi(stack, 'API', { 71 | name: 'demo', 72 | schema, 73 | }); 74 | schema.addQuery('test', new ResolvableField({ 75 | returnType: t.string, 76 | })); 77 | 78 | // THEN 79 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 80 | Definition: 'schema {\n query: Query\n}\ntype Query {\n test: String\n}\n', 81 | }); 82 | }); 83 | 84 | test('definition mode `code` allows for schema to addMutation', () => { 85 | // WHEN 86 | const schema = new CodeFirstSchema(); 87 | new appsync.GraphqlApi(stack, 'API', { 88 | name: 'demo', 89 | schema, 90 | }); 91 | schema.addMutation('test', new ResolvableField({ 92 | returnType: t.string, 93 | })); 94 | 95 | // THEN 96 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 97 | Definition: 'schema {\n mutation: Mutation\n}\ntype Mutation {\n test: String\n}\n', 98 | }); 99 | }); 100 | 101 | test('definition mode `code` allows for schema to addSubscription', () => { 102 | // WHEN 103 | const schema = new CodeFirstSchema(); 104 | new appsync.GraphqlApi(stack, 'API', { 105 | name: 'demo', 106 | schema, 107 | }); 108 | schema.addSubscription('test', new ResolvableField({ 109 | returnType: t.string, 110 | })); 111 | 112 | // THEN 113 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 114 | Definition: 'schema {\n subscription: Subscription\n}\ntype Subscription {\n test: String\n}\n', 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/appsync-union-types.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as t from './scalar-type-defintions'; 5 | import { CodeFirstSchema, Field, InterfaceType, ObjectType, ResolvableField, UnionType } from '../src'; 6 | 7 | const out = 'type Test1 {\n test1: String\n}\ntype Test2 {\n test2: String\n}\nunion UnionTest = Test1 | Test2\n'; 8 | const test1 = new ObjectType('Test1', { 9 | definition: { test1: t.string }, 10 | }); 11 | const test2 = new ObjectType('Test2', { 12 | definition: { test2: t.string }, 13 | }); 14 | let stack: cdk.Stack; 15 | let schema: CodeFirstSchema; 16 | beforeEach(() => { 17 | // GIVEN 18 | stack = new cdk.Stack(); 19 | schema = new CodeFirstSchema(); 20 | new appsync.GraphqlApi(stack, 'api', { 21 | name: 'api', 22 | schema, 23 | }); 24 | schema.addType(test1); 25 | schema.addType(test2); 26 | }); 27 | 28 | describe('testing Union Type properties', () => { 29 | test('UnionType configures properly', () => { 30 | // WHEN 31 | const union = new UnionType('UnionTest', { 32 | definition: [test1, test2], 33 | }); 34 | schema.addType(union); 35 | // THEN 36 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 37 | Definition: `${out}`, 38 | }); 39 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::Resolver', 0); 40 | }); 41 | 42 | test('UnionType can addField', () => { 43 | // WHEN 44 | const union = new UnionType('UnionTest', { 45 | definition: [test1], 46 | }); 47 | schema.addType(union); 48 | union.addField({ field: test2.attribute() }); 49 | 50 | // THEN 51 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 52 | Definition: `${out}`, 53 | }); 54 | }); 55 | 56 | test('UnionType errors when addField is configured with fieldName option', () => { 57 | // WHEN 58 | const union = new UnionType('UnionTest', { 59 | definition: [test1], 60 | }); 61 | schema.addType(union); 62 | 63 | // THEN 64 | expect(() => { 65 | union.addField({ fieldName: 'fail', field: test2.attribute() }); 66 | }).toThrowError('Union Types cannot be configured with the fieldName option. Use the field option instead.'); 67 | }); 68 | 69 | test('UnionType errors when addField is not configured with field option', () => { 70 | // WHEN 71 | const union = new UnionType('UnionTest', { 72 | definition: [test1], 73 | }); 74 | schema.addType(union); 75 | 76 | // THEN 77 | expect(() => { 78 | union.addField({}); 79 | }).toThrowError('Union Types must be configured with the field option.'); 80 | }); 81 | 82 | test('UnionType can be a GraphqlType', () => { 83 | // WHEN 84 | const union = new UnionType('UnionTest', { 85 | definition: [test1, test2], 86 | }); 87 | schema.addType(union); 88 | 89 | schema.addType(new ObjectType('Test2', { 90 | definition: { union: union.attribute() }, 91 | })); 92 | 93 | const obj = 'type Test2 {\n union: UnionTest\n}\n'; 94 | 95 | // THEN 96 | Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLSchema', { 97 | Definition: `${out}${obj}`, 98 | }); 99 | }); 100 | 101 | test('appsync errors when addField with Graphql Types', () => { 102 | // WHEN 103 | const test = new UnionType('Test', { 104 | definition: [], 105 | }); 106 | // THEN 107 | expect(() => { 108 | test.addField({ field: t.string }); 109 | }).toThrowError('Fields for Union Types must be Object Types.'); 110 | }); 111 | 112 | test('appsync errors when addField with Field', () => { 113 | // WHEN 114 | const test = new UnionType('Test', { 115 | definition: [], 116 | }); 117 | // THEN 118 | expect(() => { 119 | test.addField({ field: new Field({ returnType: t.string }) }); 120 | }).toThrowError('Fields for Union Types must be Object Types.'); 121 | }); 122 | 123 | test('appsync errors when addField with ResolvableField', () => { 124 | // WHEN 125 | const test = new UnionType('Test', { 126 | definition: [], 127 | }); 128 | // THEN 129 | expect(() => { 130 | test.addField({ field: new ResolvableField({ returnType: t.string }) }); 131 | }).toThrowError('Fields for Union Types must be Object Types.'); 132 | }); 133 | 134 | test('appsync errors when addField with Interface Types', () => { 135 | // WHEN 136 | const test = new UnionType('Test', { 137 | definition: [], 138 | }); 139 | // THEN 140 | expect(() => { 141 | test.addField({ field: new InterfaceType('break', { definition: {} }).attribute() }); 142 | }).toThrowError('Fields for Union Types must be Object Types.'); 143 | }); 144 | 145 | test('appsync errors when addField with Union Types', () => { 146 | // WHEN 147 | const test = new UnionType('Test', { 148 | definition: [], 149 | }); 150 | // THEN 151 | expect(() => { 152 | test.addField({ field: test.attribute() }); 153 | }).toThrowError('Fields for Union Types must be Object Types.'); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/appsync.test.graphql: -------------------------------------------------------------------------------- 1 | type test { 2 | version: String! 3 | } 4 | type Query { 5 | getTests: [test]! 6 | } 7 | type Mutation { 8 | addTest(version: String!): test 9 | } -------------------------------------------------------------------------------- /test/integ.graphql-schema.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 3 | import * as db from 'aws-cdk-lib/aws-dynamodb'; 4 | import * as ObjectTypeDefs from './object-type-definitions'; 5 | import * as ScalarType from './scalar-type-defintions'; 6 | import { CodeFirstSchema, Directive, EnumType, Field, InputType, InterfaceType, ObjectType, ResolvableField, UnionType } from '../src'; 7 | 8 | /* 9 | * Creates an Appsync GraphQL API and schema in a code-first approach. 10 | * 11 | * Stack verification steps: 12 | * Deploy stack, get api key and endpoinScalarType. Check if schema connects to data source. 13 | * 14 | * -- bash verify.integ.graphql-schema.sh --start -- start -- 15 | * -- aws appsync list-graphql-apis -- obtain apiId & endpoint -- 16 | * -- aws appsync list-api-keys --api-id [apiId] -- obtain api key -- 17 | * -- bash verify.integ.graphql-schema.sh --check [apiKey] [url] -- check if success -- 18 | * -- bash verify.integ.graphql-schema.sh --clean -- clean -- 19 | */ 20 | const app = new cdk.App(); 21 | const stack = new cdk.Stack(app, 'code-first-schema'); 22 | 23 | const schema = new CodeFirstSchema(); 24 | 25 | const node = new InterfaceType('Node', { 26 | definition: { 27 | created: ScalarType.string, 28 | edited: ScalarType.string, 29 | id: ScalarType.required_id, 30 | }, 31 | }); 32 | 33 | schema.addType(node); 34 | 35 | const api = new appsync.GraphqlApi(stack, 'code-first-api', { 36 | name: 'api', 37 | schema: schema, 38 | }); 39 | 40 | const table = new db.Table(stack, 'table', { 41 | partitionKey: { 42 | name: 'id', 43 | type: db.AttributeType.STRING, 44 | }, 45 | }); 46 | 47 | const tableDS = api.addDynamoDbDataSource('planets', table); 48 | 49 | const planet = ObjectTypeDefs.planet; 50 | schema.addType(planet); 51 | 52 | const species = schema.addType(new ObjectType('Species', { 53 | interfaceTypes: [node], 54 | definition: { 55 | name: ScalarType.string, 56 | classification: ScalarType.string, 57 | designation: ScalarType.string, 58 | averageHeight: ScalarType.float, 59 | averageLifespan: ScalarType.int, 60 | eyeColors: ScalarType.list_string, 61 | hairColors: ScalarType.list_string, 62 | skinColors: ScalarType.list_string, 63 | language: ScalarType.string, 64 | homeworld: planet.attribute(), 65 | }, 66 | })); 67 | 68 | schema.addQuery('getPlanets', new ResolvableField({ 69 | returnType: planet.attribute({ isList: true }), 70 | dataSource: tableDS, 71 | requestMappingTemplate: appsync.MappingTemplate.dynamoDbScanTable(), 72 | responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultList(), 73 | })); 74 | 75 | /* ATTRIBUTES */ 76 | const name = new appsync.Assign('name', '$context.arguments.name'); 77 | const diameter = new appsync.Assign('diameter', '$context.arguments.diameter'); 78 | const rotationPeriod = new appsync.Assign('rotationPeriod', '$context.arguments.rotationPeriod'); 79 | const orbitalPeriod = new appsync.Assign('orbitalPeriod', '$context.arguments.orbitalPeriod'); 80 | const gravity = new appsync.Assign('gravityPeriod', '$context.arguments.gravity'); 81 | const population = new appsync.Assign('population', '$context.arguments.population'); 82 | const climates = new appsync.Assign('climates', '$context.arguments.climates'); 83 | const terrains = new appsync.Assign('terrains', '$context.arguments.terrains'); 84 | const surfaceWater = new appsync.Assign('surfaceWater', '$context.arguments.surfaceWater'); 85 | schema.addMutation('addPlanet', new ResolvableField({ 86 | returnType: planet.attribute(), 87 | args: { 88 | name: ScalarType.string, 89 | diameter: ScalarType.int, 90 | rotationPeriod: ScalarType.int, 91 | orbitalPeriod: ScalarType.int, 92 | gravity: ScalarType.string, 93 | population: ScalarType.list_string, 94 | climates: ScalarType.list_string, 95 | terrains: ScalarType.list_string, 96 | surfaceWater: ScalarType.float, 97 | }, 98 | dataSource: tableDS, 99 | requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem( 100 | appsync.PrimaryKey.partition('id').auto(), new appsync.AttributeValues('$context.arguments', 101 | [name, diameter, rotationPeriod, orbitalPeriod, gravity, population, climates, terrains, surfaceWater], 102 | ), 103 | ), 104 | responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(), 105 | })); 106 | 107 | schema.addSubscription('addedPlanets', new Field({ 108 | returnType: planet.attribute(), 109 | args: { id: ScalarType.required_id }, 110 | directives: [Directive.subscribe('addPlanet')], 111 | })); 112 | schema.addType(new InputType('AwesomeInput', { 113 | definition: { awesomeInput: ScalarType.string }, 114 | })); 115 | 116 | schema.addType(new EnumType('Episodes', { 117 | definition: [ 118 | 'The_Phantom_Menace', 119 | 'Attack_of_the_Clones', 120 | 'Revenge_of_the_Sith', 121 | 'A_New_Hope', 122 | 'The_Empire_Strikes_Back', 123 | 'Return_of_the_Jedi', 124 | 'The_Force_Awakens', 125 | 'The_Last_Jedi', 126 | 'The_Rise_of_Skywalker', 127 | ], 128 | })); 129 | 130 | schema.addType(new UnionType('Union', { 131 | definition: [species, planet], 132 | })); 133 | 134 | app.synth(); 135 | -------------------------------------------------------------------------------- /test/object-type-definitions.ts: -------------------------------------------------------------------------------- 1 | import * as ScalarType from './scalar-type-defintions'; 2 | import { ObjectType } from '../src'; 3 | 4 | export const planet = new ObjectType('Planet', { 5 | definition: { 6 | name: ScalarType.string, 7 | diameter: ScalarType.int, 8 | rotationPeriod: ScalarType.int, 9 | orbitalPeriod: ScalarType.int, 10 | gravity: ScalarType.string, 11 | population: ScalarType.list_string, 12 | climates: ScalarType.list_string, 13 | terrains: ScalarType.list_string, 14 | surfaceWater: ScalarType.float, 15 | created: ScalarType.string, 16 | edited: ScalarType.string, 17 | id: ScalarType.required_id, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /test/scalar-type-defintions.ts: -------------------------------------------------------------------------------- 1 | import { GraphqlType } from '../src'; 2 | 3 | // ID 4 | export const id = GraphqlType.id(); 5 | export const list_id = GraphqlType.id({ 6 | isList: true, 7 | }); 8 | export const required_id = GraphqlType.id({ 9 | isRequired: true, 10 | }); 11 | export const required_list_id = GraphqlType.id({ 12 | isRequiredList: true, 13 | }); 14 | export const required_list_required_id = GraphqlType.id({ 15 | isRequired: true, 16 | isRequiredList: true, 17 | }); 18 | export const dup_id = GraphqlType.id({ 19 | isList: true, 20 | isRequired: true, 21 | isRequiredList: true, 22 | }); 23 | 24 | // STRING 25 | export const string = GraphqlType.string(); 26 | export const required_string = GraphqlType.string({ isRequired: true }); 27 | export const list_string = GraphqlType.string({ isList: true }); 28 | 29 | // INT 30 | export const int = GraphqlType.int(); 31 | 32 | // FLOAT 33 | export const float = GraphqlType.float(); 34 | 35 | // BOOLEAN 36 | export const boolean = GraphqlType.boolean(); 37 | 38 | // AWSDate 39 | export const awsDate = GraphqlType.awsDate(); 40 | 41 | // AWSTime 42 | export const awsTime = GraphqlType.awsTime(); 43 | 44 | // AWSDateTime 45 | export const awsDateTime = GraphqlType.awsDateTime(); 46 | 47 | // AWSTimestamp 48 | export const awsTimestamp = GraphqlType.awsTimestamp(); 49 | 50 | // AWSEmail 51 | export const awsEmail = GraphqlType.awsEmail(); 52 | 53 | // AWSJSON 54 | export const awsJson = GraphqlType.awsJson(); 55 | 56 | // AWSUrl 57 | export const awsUrl = GraphqlType.awsUrl(); 58 | 59 | // AWSPhone 60 | export const awsPhone = GraphqlType.awsPhone(); 61 | 62 | // AWSIPAddress 63 | export const awsIpAddress = GraphqlType.awsIpAddress(); 64 | -------------------------------------------------------------------------------- /test/source-api-association-merge.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 4 | import * as iam from 'aws-cdk-lib/aws-iam'; 5 | import * as cdk from 'aws-cdk-lib/core'; 6 | import { SourceApiAssociationMergeOperation } from '../src'; 7 | 8 | let stack: cdk.Stack; 9 | let api1: appsync.IGraphqlApi; 10 | let api2: appsync.IGraphqlApi; 11 | let mergedApiExecutionRole: iam.Role; 12 | let mergedApi: appsync.IGraphqlApi; 13 | let sourceApiAssociation1: appsync.SourceApiAssociation; 14 | let sourceApiAssociation2: appsync.SourceApiAssociation; 15 | beforeEach(() => { 16 | stack = new cdk.Stack(); 17 | 18 | api1 = new appsync.GraphqlApi(stack, 'api1', { 19 | authorizationConfig: {}, 20 | name: 'api', 21 | definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), 22 | logConfig: {}, 23 | }); 24 | 25 | api2 = new appsync.GraphqlApi(stack, 'api2', { 26 | authorizationConfig: {}, 27 | name: 'api', 28 | definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.test.graphql')), 29 | logConfig: {}, 30 | }); 31 | 32 | mergedApiExecutionRole = new iam.Role(stack, 'MergedApiExecutionRole', { 33 | assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), 34 | }); 35 | 36 | mergedApi = new appsync.GraphqlApi(stack, 'merged-api', { 37 | name: 'api', 38 | definition: appsync.Definition.fromSourceApis({ 39 | sourceApis: [], 40 | mergedApiExecutionRole: mergedApiExecutionRole, 41 | }), 42 | }); 43 | 44 | sourceApiAssociation1 = new appsync.SourceApiAssociation(stack, 'SourceApiAssociation1', { 45 | sourceApi: api1, 46 | mergedApi: mergedApi, 47 | mergeType: appsync.MergeType.MANUAL_MERGE, 48 | mergedApiExecutionRole: mergedApiExecutionRole, 49 | }); 50 | 51 | sourceApiAssociation2 = new appsync.SourceApiAssociation(stack, 'SourceApiAssociation2', { 52 | sourceApi: api2, 53 | mergedApi: mergedApi, 54 | mergeType: appsync.MergeType.MANUAL_MERGE, 55 | mergedApiExecutionRole: mergedApiExecutionRole, 56 | }); 57 | }); 58 | 59 | test('source api association merge operation with version identifier', () => { 60 | new SourceApiAssociationMergeOperation(stack, 'SourceApi1Merge', { 61 | sourceApiAssociation: sourceApiAssociation1, 62 | versionIdentifier: '1', 63 | }); 64 | 65 | Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 5); 66 | Template.fromStack(stack).resourceCountIs('Custom::AppSyncSourceApiMergeOperation', 1); 67 | 68 | Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { 69 | Handler: 'index.isComplete', 70 | Timeout: 120, 71 | }); 72 | 73 | Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { 74 | Handler: 'index.onEvent', 75 | Timeout: 120, 76 | }); 77 | 78 | Template.fromStack(stack).hasResourceProperties('Custom::AppSyncSourceApiMergeOperation', { 79 | ServiceToken: { 80 | 'Fn::GetAtt': [ 81 | 'SchemaMergeOperationProviderframeworkonEventC84B1BAA', 82 | 'Arn', 83 | ], 84 | }, 85 | associationId: { 86 | 'Fn::GetAtt': [ 87 | 'SourceApiAssociation17B8F97C7', 88 | 'AssociationId', 89 | ], 90 | }, 91 | mergedApiIdentifier: { 92 | 'Fn::GetAtt': [ 93 | 'mergedapiCE4CAF34', 94 | 'Arn', 95 | ], 96 | }, 97 | sourceApiIdentifier: { 98 | 'Fn::GetAtt': [ 99 | 'api1A91238E2', 100 | 'Arn', 101 | ], 102 | }, 103 | versionIdentifier: '1', 104 | }); 105 | }); 106 | 107 | test('source api association merge operation with always update', () => { 108 | new SourceApiAssociationMergeOperation(stack, 'SourceApi1Merge', { 109 | sourceApiAssociation: sourceApiAssociation1, 110 | alwaysMergeOnStackUpdate: true, 111 | }); 112 | 113 | Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 5); 114 | Template.fromStack(stack).resourceCountIs('Custom::AppSyncSourceApiMergeOperation', 1); 115 | }); 116 | 117 | test('source api association merge operations with version identifier', () => { 118 | new SourceApiAssociationMergeOperation(stack, 'SourceApi1Merge', { 119 | sourceApiAssociation: sourceApiAssociation1, 120 | versionIdentifier: '1', 121 | }); 122 | 123 | new SourceApiAssociationMergeOperation(stack, 'SourceApi2Merge', { 124 | sourceApiAssociation: sourceApiAssociation2, 125 | versionIdentifier: '1', 126 | }); 127 | 128 | Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 5); 129 | Template.fromStack(stack).resourceCountIs('Custom::AppSyncSourceApiMergeOperation', 2); 130 | 131 | Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { 132 | Handler: 'index.isComplete', 133 | Timeout: 120, 134 | }); 135 | 136 | Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { 137 | Handler: 'index.onEvent', 138 | Timeout: 120, 139 | }); 140 | 141 | Template.fromStack(stack).hasResourceProperties('Custom::AppSyncSourceApiMergeOperation', { 142 | ServiceToken: { 143 | 'Fn::GetAtt': [ 144 | 'SchemaMergeOperationProviderframeworkonEventC84B1BAA', 145 | 'Arn', 146 | ], 147 | }, 148 | associationId: { 149 | 'Fn::GetAtt': [ 150 | 'SourceApiAssociation17B8F97C7', 151 | 'AssociationId', 152 | ], 153 | }, 154 | mergedApiIdentifier: { 155 | 'Fn::GetAtt': [ 156 | 'mergedapiCE4CAF34', 157 | 'Arn', 158 | ], 159 | }, 160 | sourceApiIdentifier: { 161 | 'Fn::GetAtt': [ 162 | 'api1A91238E2', 163 | 'Arn', 164 | ], 165 | }, 166 | versionIdentifier: '1', 167 | }); 168 | 169 | Template.fromStack(stack).hasResourceProperties('Custom::AppSyncSourceApiMergeOperation', { 170 | ServiceToken: { 171 | 'Fn::GetAtt': [ 172 | 'SchemaMergeOperationProviderframeworkonEventC84B1BAA', 173 | 'Arn', 174 | ], 175 | }, 176 | associationId: { 177 | 'Fn::GetAtt': [ 178 | 'SourceApiAssociation254340D38', 179 | 'AssociationId', 180 | ], 181 | }, 182 | mergedApiIdentifier: { 183 | 'Fn::GetAtt': [ 184 | 'mergedapiCE4CAF34', 185 | 'Arn', 186 | ], 187 | }, 188 | sourceApiIdentifier: { 189 | 'Fn::GetAtt': [ 190 | 'api2C4850CEA', 191 | 'Arn', 192 | ], 193 | }, 194 | versionIdentifier: '1', 195 | }); 196 | }); -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2020" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2020" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------