├── .eslintignore
├── .eslintrc.js
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── build.yml
│ ├── pre-publish.yml
│ └── publish.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── codecov.yml
├── docs
└── API.md
├── jest.config.js
├── lib
├── index.ts
└── transformer
│ ├── cdk-transformer.ts
│ ├── schema-transformer.ts
│ └── transform.conf.json
├── package-lock.json
├── package.json
├── test
└── index.test.ts
└── testSchema.graphql
/.eslintignore:
--------------------------------------------------------------------------------
1 | # don't ever lint node_modules
2 | node_modules
3 |
4 | # don't lint build output
5 | lib
6 |
7 | # don't lint nyc coverage output
8 | coverage
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env commonjs */
2 |
3 | module.exports = {
4 | root: true,
5 | parser: '@typescript-eslint/parser',
6 | plugins: [
7 | '@typescript-eslint',
8 | ],
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:@typescript-eslint/eslint-recommended',
12 | 'plugin:@typescript-eslint/recommended',
13 | ],
14 | };
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ----
4 |
5 | *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
6 |
7 | Please read the contribution guidelines and follow the pull-request checklist:
8 | https://github.com/kcwinner/appsync-transformer-construct/blob/main/CONTRIBUTING.md
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - "*"
7 | tags-ignore:
8 | - "v*"
9 | pull_request:
10 | branches: [main]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [10.x, 12.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - run: npm install
27 | - run: npm run build --if-present
28 | - run: npm test
29 | - name: Codecov
30 | uses: codecov/codecov-action@v1.0.5
31 | with:
32 | token: ${{ secrets.CODECOV_TOKEN }}
33 | node-version: 12.x
34 | env:
35 | CI: true
--------------------------------------------------------------------------------
/.github/workflows/pre-publish.yml:
--------------------------------------------------------------------------------
1 | name: jsii pre-publish checks
2 |
3 | on:
4 | push:
5 | branches:
6 | - "*"
7 | tags-ignore:
8 | - "v*"
9 | pull_request:
10 | branches: [main]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [12.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | with:
23 | fetch-depth: 1
24 |
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 |
30 | - run: make
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: jsii publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [12.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 1
20 |
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 |
26 | - run: make
27 |
28 | - run: make publish-npm
29 | env:
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 |
32 | - run: make publish-pypi
33 | env:
34 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
35 |
36 | # - run: make publish-nuget
37 | # env:
38 | # NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
39 |
40 | # - run: make publish-maven
41 | # env:
42 | # MAVEN_STAGING_PROFILE_ID: ${{ secrets.MAVEN_STAGING_PROFILE_ID }}
43 | # MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
44 | # MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
45 | # MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
46 | # MAVEN_GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE }}
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # npm
3 | node_modules/
4 | .npm
5 | *.tgz
6 |
7 | # code coverage
8 | coverage/
9 |
10 | # jsii
11 | tsconfig.json
12 | .jsii
13 | dist/
14 | *.js
15 | *.d.ts
16 | .cdk.staging
17 |
18 | # My transform directory
19 | appsync/*
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | tsconfig.json
3 | tslint.json
4 | .prettierrc
5 | .eslintignore
6 | .eslintrc.js
7 |
8 | # Exclude jsii outdir
9 | dist
10 |
11 | # Include .jsii
12 | !.jsii
13 |
14 | # Docs and test stuff
15 | testSchema.graphql
16 | CONTRIBUTING.md
17 | .github/*
18 | appsync/*
19 | coverage/*
20 | codecov.yml
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## 1.63.0-rc.3 (2020-09-30)
6 |
7 | ### Changes
8 |
9 | * Pinning AWS CDK version to 1.63.0
10 |
11 | ## 1.63.0-rc.2 (2020-09-30)
12 |
13 | ### Bugfixes
14 |
15 | * Fixed a bug with @key directive where it wouldn't properly create resolvers when the @key had a custom query field
16 | * Cleaned up the way resolvers are mapped to tables
17 |
18 | ## 1.63.0-rc.1 (2020-09-24)
19 |
20 | ### Updates / Bugfixes
21 |
22 | * Updating for changes to CDK v1.63+
23 |
24 | ## 1.50.0-rc.1 (2020-08-30)
25 |
26 | ### Features
27 |
28 | * Adding in experimental support for function directives. See README for details.
29 |
30 | ## 1.50.0-alpha (2020-07-07)
31 |
32 | ### Bugfixes
33 |
34 | * Fixed circular reference with the nested stack
35 | * Changed scope of the `appsyncGraphQLEndpointOutput` to correctly be the main stack
36 |
37 | ## 1.49.1-alpha (2020-07-06)
38 |
39 | ### Features
40 |
41 | * Initial release
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes new environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent. The versioning scheme we use is in line with the supported AWS CDK version.
16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
17 | do not have permission to do that, you may request the second reviewer to merge it for you.
18 |
19 | ## Code of Conduct
20 |
21 | ### Our Pledge
22 |
23 | In the interest of fostering an open and welcoming environment, we as
24 | contributors and maintainers pledge to making participation in our project and
25 | our community a harassment-free experience for everyone, regardless of age, body
26 | size, disability, ethnicity, gender identity and expression, level of experience,
27 | nationality, personal appearance, race, religion, or sexual identity and
28 | orientation.
29 |
30 | ### Our Standards
31 |
32 | Examples of behavior that contributes to creating a positive environment
33 | include:
34 |
35 | * Using welcoming and inclusive language
36 | * Being respectful of differing viewpoints and experiences
37 | * Gracefully accepting constructive criticism
38 | * Focusing on what is best for the community
39 | * Showing empathy towards other community members
40 |
41 | Examples of unacceptable behavior by participants include:
42 |
43 | * The use of sexualized language or imagery and unwelcome sexual attention or
44 | advances
45 | * Trolling, insulting/derogatory comments, and personal or political attacks
46 | * Public or private harassment
47 | * Publishing others' private information, such as a physical or electronic
48 | address, without explicit permission
49 | * Other conduct which could reasonably be considered inappropriate in a
50 | professional setting
51 |
52 | ### Our Responsibilities
53 |
54 | Project maintainers are responsible for clarifying the standards of acceptable
55 | behavior and are expected to take appropriate and fair corrective action in
56 | response to any instances of unacceptable behavior.
57 |
58 | Project maintainers have the right and responsibility to remove, edit, or
59 | reject comments, commits, code, wiki edits, issues, and other contributions
60 | that are not aligned to this Code of Conduct, or to ban temporarily or
61 | permanently any contributor for other behaviors that they deem inappropriate,
62 | threatening, offensive, or harmful.
63 |
64 | ### Scope
65 |
66 | This Code of Conduct applies both within project spaces and in public spaces
67 | when an individual is representing the project or its community. Examples of
68 | representing a project or community include using an official project e-mail
69 | address, posting via an official social media account, or acting as an appointed
70 | representative at an online or offline event. Representation of a project may be
71 | further defined and clarified by project maintainers.
72 |
73 | ### Enforcement
74 |
75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
77 | complaints will be reviewed and investigated and will result in a response that
78 | is deemed necessary and appropriate to the circumstances. The project team is
79 | obligated to maintain confidentiality with regard to the reporter of an incident.
80 | Further details of specific enforcement policies may be posted separately.
81 |
82 | Project maintainers who do not follow or enforce the Code of Conduct in good
83 | faith may face temporary or permanent repercussions as determined by other
84 | members of the project's leadership.
85 |
86 | ### Attribution
87 |
88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
89 | available at [http://contributor-covenant.org/version/1/4][version]
90 |
91 | [homepage]: http://contributor-covenant.org
92 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash -o pipefail
2 |
3 | DOCKER_IMAGE := jsii/superchain
4 | DOCKER_TAG := latest
5 | DOCKER_WORKDIR := /workdir
6 |
7 | build:
8 | docker run \
9 | --workdir ${DOCKER_WORKDIR} \
10 | --volume ${PWD}:${DOCKER_WORKDIR} \
11 | ${DOCKER_IMAGE}:${DOCKER_TAG} \
12 | /bin/bash -c "rm -rf dist && npm i && npm run package"
13 |
14 | publish-npm:
15 | docker run \
16 | --workdir ${DOCKER_WORKDIR} \
17 | --volume ${PWD}:${DOCKER_WORKDIR} \
18 | --env NPM_TOKEN \
19 | ${DOCKER_IMAGE}:${DOCKER_TAG} \
20 | /bin/bash -c "npx jsii-release-npm dist/js"
21 |
22 | publish-nuget:
23 | docker run \
24 | --workdir ${DOCKER_WORKDIR} \
25 | --volume ${PWD}:${DOCKER_WORKDIR} \
26 | --env NUGET_API_KEY \
27 | ${DOCKER_IMAGE}:${DOCKER_TAG} \
28 | /bin/bash -c "npx jsii-release-nuget dist/dotnet"
29 |
30 | publish-pypi:
31 | docker run \
32 | --workdir ${DOCKER_WORKDIR} \
33 | --volume ${PWD}:${DOCKER_WORKDIR} \
34 | --env TWINE_USERNAME=__token__ \
35 | --env TWINE_PASSWORD=$(PYPI_TOKEN) \
36 | ${DOCKER_IMAGE}:${DOCKER_TAG} \
37 | /bin/bash -c "npx jsii-release-pypi dist/python"
38 |
39 | publish-maven:
40 | docker run \
41 | --workdir ${DOCKER_WORKDIR} \
42 | --volume ${PWD}:${DOCKER_WORKDIR} \
43 | --env MAVEN_STAGING_PROFILE_ID \
44 | --env MAVEN_USERNAME \
45 | --env MAVEN_PASSWORD \
46 | --env MAVEN_GPG_PRIVATE_KEY \
47 | --env MAVEN_GPG_PRIVATE_KEY_PASSPHRASE \
48 | --env MAVEN_DRYRUN \
49 | ${DOCKER_IMAGE}:${DOCKER_TAG} \
50 | /bin/bash -c "npx jsii-release-maven dist/java"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AppSync Transformer Construct for AWS CDK
2 |
3 | 
4 | [](https://codecov.io/gh/kcwinner/aws-cdk-appsync-transformer)
5 | [](https://david-dm.org/kcwinner/aws-cdk-appsync-transformer)
6 | [](https://www.npmjs.com/package/aws-cdk-appsync-transformer)
7 |
8 | [](https://badge.fury.io/js/aws-cdk-appsync-transformer)
9 | [](https://badge.fury.io/py/aws-cdk-appsync-transformer)
10 |
11 | ## THIS PACKAGE HAS BEEN ARCHIVED
12 |
13 | For CDK versions > 1.63.0 see [cdk-appsync-transformer](https://github.com/kcwinner/cdk-appsync-transformer). This follows the community accepted construct naming scheme and the project now uses [projen](https://github.com/eladb/projen).
14 |
15 | ## Why This Package
16 |
17 | In April 2020 I wrote a [blog post](https://www.trek10.com/blog/appsync-with-the-aws-cloud-development-kit) on using the AWS Cloud Development Kit with AppSync. I wrote my own transformer in order to emulate AWS Amplify's method of using GraphQL directives in order to template a lot of the Schema Definition Language.
18 |
19 | This package is my attempt to convert all of that effort into a separate construct in order to clean up the process.
20 |
21 | ## How Do I Use It
22 |
23 | ### Example Usage
24 |
25 | API With Default Values
26 | ```ts
27 | import { AppSyncTransformer } from 'aws-cdk-appsync-transformer';
28 | ...
29 | new AppSyncTransformer(this, "my-cool-api", {
30 | schemaPath: 'schema.graphql'
31 | });
32 | ```
33 |
34 | schema.graphql
35 | ```graphql
36 | type Customer @model
37 | @auth(rules: [
38 | { allow: groups, groups: ["Admins"] },
39 | { allow: private, provider: iam, operations: [read, update] }
40 | ]) {
41 | id: ID!
42 | firstName: String!
43 | lastName: String!
44 | active: Boolean!
45 | address: String!
46 | }
47 |
48 | type Product @model
49 | @auth(rules: [
50 | { allow: groups, groups: ["Admins"] },
51 | { allow: public, provider: iam, operations: [read] }
52 | ]) {
53 | id: ID!
54 | name: String!
55 | description: String!
56 | price: String!
57 | active: Boolean!
58 | added: AWSDateTime!
59 | orders: [Order] @connection
60 | }
61 |
62 | type Order @model
63 | @key(fields: ["id", "productID"]) {
64 | id: ID!
65 | productID: ID!
66 | total: String!
67 | ordered: AWSDateTime!
68 | }
69 | ```
70 |
71 | ### [Supported Amplify Directives](https://docs.amplify.aws/cli/graphql-transformer/directives)
72 |
73 | Tested:
74 | * [@model](https://docs.amplify.aws/cli/graphql-transformer/directives#model)
75 | * [@auth](https://docs.amplify.aws/cli/graphql-transformer/directives#auth)
76 | * [@connection](https://docs.amplify.aws/cli/graphql-transformer/directives#connection)
77 |
78 | Experimental:
79 | * [@key](https://docs.amplify.aws/cli/graphql-transformer/directives#key)
80 | * [@versioned](https://docs.amplify.aws/cli/graphql-transformer/directives#versioned)
81 | * [@function](https://docs.amplify.aws/cli/graphql-transformer/directives#function)
82 | * These work differently here than they do in Amplify - see [Functions](#functions) below
83 |
84 | Not Yet Supported:
85 | * [@searchable](https://docs.amplify.aws/cli/graphql-transformer/directives#searchable)
86 | * [@predictions](https://docs.amplify.aws/cli/graphql-transformer/directives#predictions)
87 | * [@http](https://docs.amplify.aws/cli/graphql-transformer/directives#http)
88 |
89 | ### Authentication
90 |
91 | User Pool Authentication
92 | ```ts
93 | const userPool = new UserPool(this, 'my-cool-user-pool', {
94 | ...
95 | })
96 | ...
97 | const userPoolClient = new UserPoolClient(this, `${id}-client`, {
98 | userPool: this.userPool,
99 | ...
100 | })
101 | ...
102 | new AppSyncTransformer(this, "my-cool-api", {
103 | schemaPath: 'schema.graphql',
104 | authorizationConfig: {
105 | defaultAuthorization: {
106 | authorizationType: AuthorizationType.USER_POOL,
107 | userPoolConfig: {
108 | userPool: userPool,
109 | appIdClientRegex: userPoolClient.userPoolClientId,
110 | defaultAction: UserPoolDefaultAction.ALLOW
111 | }
112 | }
113 | }
114 | });
115 | ```
116 |
117 | #### IAM
118 |
119 | Unauth Role: TODO
120 |
121 | Auth Role: Unsupported (for now?). Authorized roles (Lambda Functions, EC2 roles, etc) are required to setup their own role permissions.
122 |
123 | ### Functions
124 |
125 | Fields with the `@function` directive will be accessible via `api.outputs.FUNCTION_RESOLVERS`. It will return an array like below.Currently these are not named and do not specify a region. There are improvements that can be made here but this simple way has worked for me so I've implemented it first. Typically I send all `@function` requests to one Lambda Function and have it route as necessary.
126 |
127 | ```js
128 | [
129 | { typeName: 'Query', fieldName: 'listUsers' },
130 | { typeName: 'Query', fieldName: 'getUser' },
131 | { typeName: 'Mutation', fieldName: 'createUser' },
132 | { typeName: 'Mutation', fieldName: 'updateUser' }
133 | ]
134 | ```
135 |
136 | ### DataStore Support
137 |
138 | 1. Pass `syncEnabled: true` to the `AppSyncTransformerProps`
139 | 1. Generate necessary exports (see [Code Generation](#code-generation) below)
140 |
141 | ### Code Generation
142 |
143 | I've written some helpers to generate code similarly to how AWS Amplify generates statements and types. You can find the code [here](https://github.com/kcwinner/advocacy/tree/master/cdk-amplify-appsync-helpers).
144 |
145 | ## Versioning
146 |
147 | I will *attempt* to align the major and minor version of this package with [AWS CDK], but always check the release descriptions for compatibility.
148 |
149 | I currently support [](https://github.com/aws/aws-cdk)
150 |
151 | ## Limitations
152 |
153 | *
154 |
155 | ## Contributing
156 |
157 | See [CONTRIBUTING](CONTRIBUTING.md) for details
158 |
159 | ## License
160 |
161 | Distributed under [Apache License, Version 2.0](LICENSE)
162 |
163 | [aws cdk]: https://aws.amazon.com/cdk
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | precision: 2
3 | round: down
4 | range: "50...100"
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | **Classes**
4 |
5 | Name|Description
6 | ----|-----------
7 | [AppSyncTransformer](#aws-cdk-appsync-transformer-appsynctransformer)|AppSyncTransformer Construct.
8 |
9 |
10 | **Structs**
11 |
12 | Name|Description
13 | ----|-----------
14 | [AppSyncTransformerProps](#aws-cdk-appsync-transformer-appsynctransformerprops)|Properties for AppSyncTransformer Construct.
15 |
16 |
17 |
18 | ## class AppSyncTransformer 🔹
19 |
20 | AppSyncTransformer Construct.
21 |
22 | __Implements__: [IConstruct](#constructs-iconstruct), [IConstruct](#aws-cdk-core-iconstruct), [IConstruct](#constructs-iconstruct), [IDependable](#aws-cdk-core-idependable)
23 | __Extends__: [Construct](#aws-cdk-core-construct)
24 |
25 | ### Initializer
26 |
27 |
28 |
29 |
30 | ```ts
31 | new AppSyncTransformer(scope: Construct, id: string, props: AppSyncTransformerProps)
32 | ```
33 |
34 | * **scope** ([Construct](#aws-cdk-core-construct)
) *No description*
35 | * **id** (string
) *No description*
36 | * **props** ([AppSyncTransformerProps](#aws-cdk-appsync-transformer-appsynctransformerprops)
) *No description*
37 | * **schemaPath** (string
) Required.
38 | * **apiName** (string
) Optional. __*Default*__: `${id}-api`
39 | * **authorizationConfig** ([AuthorizationConfig](#aws-cdk-aws-appsync-authorizationconfig)
) Optional. __*Default*__: API_KEY authorization config
40 | * **fieldLogLevel** ([FieldLogLevel](#aws-cdk-aws-appsync-fieldloglevel)
) Optional. __*Default*__: FieldLogLevel.NONE
41 | * **syncEnabled** (boolean
) Optional. __*Default*__: false
42 |
43 |
44 |
45 | ### Properties
46 |
47 |
48 | Name | Type | Description
49 | -----|------|-------------
50 | **appsyncAPI**🔹 | [GraphQLApi](#aws-cdk-aws-appsync-graphqlapi)
|
51 | **nestedAppsyncStack**🔹 | [NestedStack](#aws-cdk-core-nestedstack)
|
52 | **tableNameMap**🔹 | any
|
53 |
54 |
55 |
56 | ## struct AppSyncTransformerProps 🔹
57 |
58 |
59 | Properties for AppSyncTransformer Construct.
60 |
61 |
62 |
63 | Name | Type | Description
64 | -----|------|-------------
65 | **schemaPath**🔹 | string
| Required.
66 | **apiName**?🔹 | string
| Optional.
__*Default*__: `${id}-api`
67 | **authorizationConfig**?🔹 | [AuthorizationConfig](#aws-cdk-aws-appsync-authorizationconfig)
| Optional.
__*Default*__: API_KEY authorization config
68 | **fieldLogLevel**?🔹 | [FieldLogLevel](#aws-cdk-aws-appsync-fieldloglevel)
| Optional.
__*Default*__: FieldLogLevel.NONE
69 | **syncEnabled**?🔹 | boolean
| Optional.
__*Default*__: false
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env commonjs */
2 |
3 | module.exports = {
4 | roots: [
5 | "/test"
6 | ],
7 | testMatch: ['**/*.test.ts'],
8 | transform: {
9 | "^.+\\.tsx?$": "ts-jest"
10 | },
11 | collectCoverage: true,
12 | collectCoverageFrom: [
13 | "lib/*.ts",
14 | "!node_modules/**"
15 | ],
16 | moduleFileExtensions: [
17 | "ts",
18 | "tsx",
19 | "js",
20 | "jsx",
21 | "json",
22 | "node"
23 | ],
24 | globals: {
25 | "ts-jest": {
26 | "diagnostics": {
27 | "warnOnly": true
28 | }
29 | },
30 | "testEnvironment": "node"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { Construct, NestedStack, CfnOutput } from '@aws-cdk/core';
2 | import { GraphqlApi, AuthorizationType, FieldLogLevel, MappingTemplate, CfnDataSource, Resolver, AuthorizationConfig, Schema } from '@aws-cdk/aws-appsync';
3 | import { Table, AttributeType, ProjectionType, BillingMode } from '@aws-cdk/aws-dynamodb';
4 | import { Effect, PolicyStatement } from '@aws-cdk/aws-iam'
5 |
6 | import { SchemaTransformer } from './transformer/schema-transformer';
7 |
8 | /**
9 | * Properties for AppSyncTransformer Construct
10 | * @param schemaPath Relative path where schema.graphql exists.
11 | * @param authorizationConfig {@link AuthorizationConfig} type defining authorization for AppSync GraphqlApi. Defaults to API_KEY
12 | * @param apiName Optional string value representing the api name
13 | * @param syncEnabled Optional boolean to enable DataStore Sync Tables
14 | * @param fieldLogLevel {@link FieldLogLevel} type for AppSync GraphqlApi log level
15 | */
16 | export interface AppSyncTransformerProps {
17 | /**
18 | * Required. Relative path where schema.graphql exists
19 | */
20 | readonly schemaPath: string
21 |
22 | /**
23 | * Optional. {@link AuthorizationConfig} type defining authorization for AppSync GraphqlApi. Defaults to API_KEY
24 | * @default API_KEY authorization config
25 | */
26 | readonly authorizationConfig?: AuthorizationConfig
27 |
28 | /**
29 | * Optional. String value representing the api name
30 | * @default `${id}-api`
31 | */
32 | readonly apiName?: string
33 |
34 | /**
35 | * Optional. Boolean to enable DataStore Sync Tables
36 | * @default false
37 | */
38 | readonly syncEnabled?: boolean
39 |
40 | /**
41 | * Optional. {@link FieldLogLevel} type for AppSync GraphqlApi log level
42 | * @default FieldLogLevel.NONE
43 | */
44 | readonly fieldLogLevel?: FieldLogLevel
45 | }
46 |
47 | const defaultAuthorizationConfig: AuthorizationConfig = {
48 | defaultAuthorization: {
49 | authorizationType: AuthorizationType.API_KEY,
50 | apiKeyConfig: {
51 | description: "Auto generated API Key from construct",
52 | name: "dev"
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * AppSyncTransformer Construct
59 | */
60 | export class AppSyncTransformer extends Construct {
61 | public readonly appsyncAPI: GraphqlApi
62 | public readonly nestedAppsyncStack: NestedStack;
63 | public readonly tableNameMap: any;
64 | public readonly outputs: any;
65 | public readonly resolvers: any;
66 |
67 | private isSyncEnabled: boolean
68 | private syncTable: Table | undefined
69 |
70 | constructor(scope: Construct, id: string, props: AppSyncTransformerProps) {
71 | super(scope, id);
72 |
73 | this.isSyncEnabled = props.syncEnabled ? props.syncEnabled : false;
74 |
75 | const transformerConfiguration = {
76 | schemaPath: props.schemaPath,
77 | syncEnabled: props.syncEnabled || false
78 | }
79 |
80 | const transformer = new SchemaTransformer(transformerConfiguration);
81 | const outputs = transformer.transform();
82 | const resolvers = transformer.getResolvers();
83 |
84 | this.outputs = outputs;
85 |
86 | this.outputs.FUNCTION_RESOLVERS.forEach((resolver: any) => {
87 | switch (resolver.typeName) {
88 | case 'Query':
89 | delete resolvers[resolver.fieldName]
90 | break;
91 | case 'Mutation':
92 | delete resolvers[resolver.fieldName]
93 | break;
94 | case 'Subscription':
95 | delete resolvers[resolver.fieldName]
96 | break;
97 | }
98 | })
99 |
100 | this.resolvers = resolvers;
101 |
102 | this.nestedAppsyncStack = new NestedStack(this, `appsync-nested-stack`);
103 |
104 | // AppSync
105 | this.appsyncAPI = new GraphqlApi(this.nestedAppsyncStack, `${id}-api`, {
106 | name: props.apiName ? props.apiName : `${id}-api`,
107 | authorizationConfig: props.authorizationConfig ? props.authorizationConfig : defaultAuthorizationConfig,
108 | logConfig: {
109 | fieldLogLevel: props.fieldLogLevel ? props.fieldLogLevel : FieldLogLevel.NONE,
110 | },
111 | schema: Schema.fromAsset('./appsync/schema.graphql')
112 | })
113 |
114 | let tableData = outputs.CDK_TABLES;
115 |
116 | // Check to see if sync is enabled
117 | if (tableData['DataStore']) {
118 | this.isSyncEnabled = true
119 | this.syncTable = this.createSyncTable(tableData['DataStore']);
120 | delete tableData['DataStore'] // We don't want to create this again below so remove it from the tableData map
121 | }
122 |
123 | this.tableNameMap = this.createTablesAndResolvers(tableData, resolvers);
124 | this.createNoneDataSourceAndResolvers(outputs.NONE, resolvers);
125 |
126 | // Outputs so we can generate exports
127 | new CfnOutput(scope, 'appsyncGraphQLEndpointOutput', {
128 | value: this.appsyncAPI.graphqlUrl,
129 | description: 'Output for aws_appsync_graphqlEndpoint'
130 | })
131 | }
132 |
133 | private createNoneDataSourceAndResolvers(none: any, resolvers: any) {
134 | const noneDataSource = this.appsyncAPI.addNoneDataSource('NONE');
135 |
136 | Object.keys(none).forEach((resolverKey: any) => {
137 | let resolver = resolvers[resolverKey];
138 |
139 | new Resolver(this.nestedAppsyncStack, `${resolver.typeName}-${resolver.fieldName}-resolver`, {
140 | api: this.appsyncAPI,
141 | typeName: resolver.typeName,
142 | fieldName: resolver.fieldName,
143 | dataSource: noneDataSource,
144 | requestMappingTemplate: MappingTemplate.fromFile(resolver.requestMappingTemplate),
145 | responseMappingTemplate: MappingTemplate.fromFile(resolver.responseMappingTemplate),
146 | })
147 | })
148 | }
149 |
150 | private createTablesAndResolvers(tableData: any, resolvers: any) {
151 | const tableNameMap: any = {};
152 |
153 | Object.keys(tableData).forEach((tableKey: any) => {
154 | const table = this.createTable(tableData[tableKey]);
155 | const dataSource = this.appsyncAPI.addDynamoDbDataSource(tableKey, table);
156 |
157 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-datasource-deltasyncconfig.html
158 |
159 | if (this.isSyncEnabled && this.syncTable) {
160 | //@ts-ignore - ds is the base CfnDataSource and the db config needs to be versioned - see CfnDataSource
161 | dataSource.ds.dynamoDbConfig.versioned = true
162 |
163 | //@ts-ignore - ds is the base CfnDataSource - see CfnDataSource
164 | dataSource.ds.dynamoDbConfig.deltaSyncConfig = {
165 | baseTableTtl: '43200', // Got this value from amplify - 30 days in minutes
166 | deltaSyncTableName: this.syncTable.tableName,
167 | deltaSyncTableTtl: '30' // Got this value from amplify - 30 minutes
168 | }
169 |
170 | // Need to add permission for our datasource service role to access the sync table
171 | dataSource.grantPrincipal.addToPolicy(new PolicyStatement({
172 | effect: Effect.ALLOW,
173 | actions: [
174 | 'dynamodb:*'
175 | ],
176 | resources: [
177 | this.syncTable.tableArn
178 | ]
179 | }))
180 | }
181 |
182 | const dynamoDbConfig = dataSource.ds.dynamoDbConfig as CfnDataSource.DynamoDBConfigProperty;
183 | tableNameMap[tableKey] = dynamoDbConfig.tableName;
184 |
185 | // Loop the basic resolvers
186 | tableData[tableKey].Resolvers.forEach((resolverKey: any) => {
187 | let resolver = resolvers[resolverKey];
188 | new Resolver(this.nestedAppsyncStack, `${resolver.typeName}-${resolver.fieldName}-resolver`, {
189 | api: this.appsyncAPI,
190 | typeName: resolver.typeName,
191 | fieldName: resolver.fieldName,
192 | dataSource: dataSource,
193 | requestMappingTemplate: MappingTemplate.fromFile(resolver.requestMappingTemplate),
194 | responseMappingTemplate: MappingTemplate.fromFile(resolver.responseMappingTemplate),
195 | })
196 | });
197 |
198 | // Loop the gsi resolvers
199 | tableData[tableKey].GSIResolvers.forEach((resolverKey: any) => {
200 | let resolver = resolvers['gsi'][resolverKey];
201 | new Resolver(this.nestedAppsyncStack, `${resolver.typeName}-${resolver.fieldName}-resolver`, {
202 | api: this.appsyncAPI,
203 | typeName: resolver.typeName,
204 | fieldName: resolver.fieldName,
205 | dataSource: dataSource,
206 | requestMappingTemplate: MappingTemplate.fromFile(resolver.requestMappingTemplate),
207 | responseMappingTemplate: MappingTemplate.fromFile(resolver.responseMappingTemplate),
208 | })
209 | });
210 | });
211 |
212 | return tableNameMap;
213 | }
214 |
215 | private createTable(tableData: any) {
216 | let tableProps: any = {
217 | billingMode: BillingMode.PAY_PER_REQUEST,
218 | partitionKey: {
219 | name: tableData.PartitionKey.name,
220 | type: this.convertAttributeType(tableData.PartitionKey.type)
221 | }
222 | };
223 |
224 | if (tableData.SortKey && tableData.SortKey.name) {
225 | tableProps.sortKey = {
226 | name: tableData.SortKey.name,
227 | type: this.convertAttributeType(tableData.SortKey.type)
228 | };
229 | };
230 |
231 | if (tableData.TTL && tableData.TTL.Enabled) {
232 | tableProps.timeToLiveAttribute = tableData.TTL.AttributeName;
233 | }
234 |
235 | let table = new Table(this.nestedAppsyncStack, tableData.TableName, tableProps);
236 |
237 | if (tableData.GlobalSecondaryIndexes && tableData.GlobalSecondaryIndexes.length > 0) {
238 | tableData.GlobalSecondaryIndexes.forEach((gsi: any) => {
239 | table.addGlobalSecondaryIndex({
240 | indexName: gsi.IndexName,
241 | partitionKey: {
242 | name: gsi.PartitionKey.name,
243 | type: this.convertAttributeType(gsi.PartitionKey.type)
244 | },
245 | projectionType: this.convertProjectionType(gsi.Projection.ProjectionType)
246 | })
247 | })
248 | }
249 |
250 | return table;
251 | }
252 |
253 | // https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html
254 | private createSyncTable(tableData: any) {
255 | return new Table(this, 'appsync-api-sync-table', {
256 | billingMode: BillingMode.PAY_PER_REQUEST,
257 | partitionKey: {
258 | name: tableData.PartitionKey.name,
259 | type: this.convertAttributeType(tableData.PartitionKey.type)
260 | },
261 | sortKey: {
262 | name: tableData.SortKey.name,
263 | type: this.convertAttributeType(tableData.SortKey.type)
264 | },
265 | timeToLiveAttribute: tableData.TTL?.AttributeName || '_ttl'
266 | })
267 | }
268 |
269 | private convertAttributeType(type: string) {
270 | switch (type) {
271 | case 'S':
272 | return AttributeType.STRING
273 | case 'N':
274 | return AttributeType.NUMBER
275 | case 'B':
276 | return AttributeType.BINARY
277 | default:
278 | return AttributeType.STRING
279 | }
280 | }
281 |
282 | private convertProjectionType(type: string) {
283 | switch (type) {
284 | case 'ALL':
285 | return ProjectionType.ALL
286 | case 'INCLUDE':
287 | return ProjectionType.INCLUDE
288 | case 'KEYS_ONLY':
289 | return ProjectionType.KEYS_ONLY
290 | default:
291 | return ProjectionType.ALL
292 | }
293 | }
294 | }
--------------------------------------------------------------------------------
/lib/transformer/cdk-transformer.ts:
--------------------------------------------------------------------------------
1 | import { Transformer, TransformerContext, getFieldArguments } from "graphql-transformer-core";
2 |
3 | const graphqlTypeStatements = ['Query', 'Mutation', 'Subscription'];
4 |
5 | export class MyTransformer extends Transformer {
6 | tables: any
7 | noneDataSources: any
8 | functionResolvers: any[]
9 | resolverTableMap: any
10 | gsiResolverTableMap: any
11 |
12 | constructor() {
13 | super(
14 | 'MyTransformer',
15 | 'directive @nullable on FIELD_DEFINITION'
16 | )
17 |
18 | this.tables = {}
19 | this.noneDataSources = {}
20 | this.functionResolvers = []
21 | this.resolverTableMap = {};
22 | this.gsiResolverTableMap = {};
23 | }
24 |
25 | public after = (ctx: TransformerContext): void => {
26 | this.printWithoutFilePath(ctx);
27 |
28 | Object.keys(this.tables).forEach(tableName => {
29 | let table = this.tables[tableName];
30 | if (!table.Resolvers) table.Resolvers = [];
31 | if (!table.GSIResolvers) table.GSIResolvers = [];
32 |
33 | Object.keys(this.resolverTableMap).forEach(resolverName => {
34 | if (this.resolverTableMap[resolverName] === tableName) table.Resolvers.push(resolverName);
35 | })
36 |
37 | Object.keys(this.gsiResolverTableMap).forEach(resolverName => {
38 | if (this.gsiResolverTableMap[resolverName] === tableName) table.GSIResolvers.push(resolverName);
39 | })
40 | })
41 |
42 | ctx.setOutput('CDK_TABLES', this.tables);
43 | ctx.setOutput('NONE', this.noneDataSources);
44 |
45 | // @ts-ignore
46 | ctx.setOutput('FUNCTION_RESOLVERS', this.functionResolvers);
47 |
48 | let query = ctx.getQuery();
49 | let queryFields = getFieldArguments(query);
50 | ctx.setOutput('QUERIES', queryFields);
51 |
52 | let mutation = ctx.getMutation();
53 | let mutationFields = getFieldArguments(mutation);
54 | ctx.setOutput('MUTATIONS', mutationFields);
55 |
56 | let subscription = ctx.getSubscription();
57 | let subscriptionFields = getFieldArguments(subscription);
58 | ctx.setOutput('SUBSCRIPTIONS', subscriptionFields);
59 | }
60 |
61 | private printWithoutFilePath(ctx: TransformerContext): void {
62 | const templateResources = ctx.template.Resources
63 | if (!templateResources) return;
64 |
65 | for (const resourceName of Object.keys(templateResources)) {
66 | const resource = templateResources[resourceName]
67 | if (resource.Type === 'AWS::DynamoDB::Table') {
68 | this.buildTablesFromResource(resourceName, ctx)
69 | } else if (resource.Type === 'AWS::AppSync::Resolver') {
70 | if (resource.Properties?.DataSourceName === 'NONE') {
71 | this.noneDataSources[resource.Properties.FieldName] = {
72 | typeName: resource.Properties.TypeName,
73 | fieldName: resource.Properties.FieldName
74 | }
75 | } else if (resource.Properties?.Kind === 'PIPELINE') { // TODO: This may not be accurate but works for now
76 | let fieldName = resource.Properties?.FieldName
77 | let typeName = resource.Properties?.TypeName
78 |
79 | this.functionResolvers.push({
80 | typeName: typeName,
81 | fieldName: fieldName
82 | })
83 | } else {
84 | let typeName = resource.Properties?.TypeName;
85 | let fieldName = resource.Properties?.FieldName;
86 | let tableName = resource.Properties?.DataSourceName?.payload[0];
87 | tableName = tableName.replace('DataSource', 'Table');
88 |
89 | if (graphqlTypeStatements.indexOf(typeName) >= 0) {
90 | this.resolverTableMap[fieldName] = tableName;
91 | } else { // this is a GSI
92 | this.gsiResolverTableMap[`${typeName}${fieldName}`] = tableName;
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | private buildTablesFromResource(resourceName: string, ctx: TransformerContext): void {
100 | const tableResource = ctx.template.Resources ? ctx.template.Resources[resourceName] : undefined
101 |
102 | const attributeDefinitions = tableResource?.Properties?.AttributeDefinitions
103 | const keySchema = tableResource?.Properties?.KeySchema
104 |
105 | let keys = this.parseKeySchema(keySchema, attributeDefinitions);
106 |
107 | let table = {
108 | TableName: resourceName,
109 | PartitionKey: keys.partitionKey,
110 | SortKey: keys.sortKey,
111 | TTL: tableResource?.Properties?.TimeToLiveSpecification,
112 | GlobalSecondaryIndexes: [] as any[]
113 | }
114 |
115 | const gsis = tableResource?.Properties?.GlobalSecondaryIndexes;
116 | if (gsis) {
117 | gsis.forEach((gsi: any) => {
118 | let gsiKeys = this.parseKeySchema(gsi.KeySchema, attributeDefinitions);
119 | let gsiDefinition = {
120 | IndexName: gsi.IndexName,
121 | Projection: gsi.Projection,
122 | PartitionKey: gsiKeys.partitionKey,
123 | SortKey: gsiKeys.sortKey,
124 | }
125 |
126 | table.GlobalSecondaryIndexes.push(gsiDefinition);
127 | })
128 | }
129 |
130 | this.tables[resourceName] = table
131 | }
132 |
133 | private parseKeySchema(keySchema: any, attributeDefinitions: any) {
134 | let partitionKey: any = {}
135 | let sortKey: any = {}
136 |
137 | keySchema.forEach((key: any) => {
138 | let keyType = key.KeyType
139 | let attributeName = key.AttributeName
140 |
141 | let attribute = attributeDefinitions.find((attribute: any) => {
142 | return attribute.AttributeName === attributeName
143 | })
144 |
145 | if (keyType === 'HASH') {
146 | partitionKey = {
147 | name: attribute.AttributeName,
148 | type: attribute.AttributeType
149 | }
150 | } else if (keyType === 'RANGE') {
151 | sortKey = {
152 | name: attribute.AttributeName,
153 | type: attribute.AttributeType
154 | }
155 | }
156 | })
157 |
158 | return {
159 | partitionKey,
160 | sortKey
161 | }
162 | }
163 | }
--------------------------------------------------------------------------------
/lib/transformer/schema-transformer.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLTransform } from 'graphql-transformer-core';
2 | import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer';
3 | import { ModelConnectionTransformer } from 'graphql-connection-transformer';
4 | import { KeyTransformer } from 'graphql-key-transformer';
5 | import { VersionedModelTransformer } from 'graphql-versioned-transformer';
6 | import { ModelAuthTransformer, ModelAuthTransformerConfig } from 'graphql-auth-transformer'
7 |
8 | import { TransformConfig, TRANSFORM_CURRENT_VERSION, TRANSFORM_CONFIG_FILE_NAME } from 'graphql-transformer-core/lib/util/transformConfig';
9 |
10 | // Import this way because FunctionTransformer.d.ts types were throwing an eror. And we didn't write this package so hope for the best :P
11 | const { FunctionTransformer } = require('graphql-function-transformer');
12 |
13 | import Resource from "cloudform-types/types/resource";
14 |
15 | import { MyTransformer } from './cdk-transformer';
16 |
17 | import { normalize, join } from 'path';
18 | import * as fs from "fs";
19 |
20 | export interface SchemaTransformerProps {
21 | schemaPath: string
22 | outputPath?: string
23 | deletionProtectionEnabled?: boolean
24 | syncEnabled?: boolean
25 | }
26 |
27 | export class SchemaTransformer {
28 | outputs: any
29 | resolvers: any
30 | schemaPath: string
31 | outputPath: string
32 | isSyncEnabled: boolean
33 | authRolePolicy: Resource | undefined
34 | unauthRolePolicy: Resource | undefined
35 | authTransformerConfig: ModelAuthTransformerConfig
36 |
37 | constructor(props: SchemaTransformerProps) {
38 | this.resolvers = {}
39 |
40 | this.schemaPath = props.schemaPath || './schema.graphql';
41 | this.outputPath = props.outputPath || './appsync';
42 | this.isSyncEnabled = props.syncEnabled || false
43 |
44 | // TODO: Make this mo betta
45 | this.authTransformerConfig = {
46 | authConfig: {
47 | defaultAuthentication: {
48 | authenticationType: 'AMAZON_COGNITO_USER_POOLS',
49 | userPoolConfig: {
50 | userPoolId: '12345xyz'
51 | }
52 | },
53 | additionalAuthenticationProviders: [
54 | {
55 | authenticationType: 'API_KEY',
56 | apiKeyConfig: {
57 | description: 'Testing',
58 | apiKeyExpirationDays: 100
59 | }
60 | },
61 | {
62 | authenticationType: 'AWS_IAM'
63 | }
64 | ]
65 | }
66 | }
67 | }
68 |
69 | transform() {
70 | let transformConfig = this.isSyncEnabled ? this.loadConfigSync('lib/transformer/') : {}
71 |
72 | // Note: This is not exact as we are omitting the @searchable transformer.
73 | const transformer = new GraphQLTransform({
74 | transformConfig: transformConfig,
75 | transformers: [
76 | new DynamoDBModelTransformer(),
77 | new VersionedModelTransformer(),
78 | new FunctionTransformer(),
79 | new KeyTransformer(),
80 | new ModelConnectionTransformer(),
81 | new ModelAuthTransformer(this.authTransformerConfig),
82 | new MyTransformer(),
83 | ]
84 | })
85 |
86 | const schema = fs.readFileSync(this.schemaPath);
87 | const cfdoc = transformer.transform(schema.toString());
88 |
89 | // TODO: Get Unauth Role and Auth Role policies for authorization stuff
90 | this.authRolePolicy = cfdoc.rootStack.Resources?.AuthRolePolicy01 as Resource || undefined
91 | this.unauthRolePolicy = cfdoc.rootStack.Resources?.UnauthRolePolicy01 as Resource || undefined
92 |
93 | this.writeSchema(cfdoc.schema);
94 | this.writeResolversToFile(cfdoc.resolvers);
95 |
96 | this.outputs = cfdoc.rootStack.Outputs;
97 |
98 | return this.outputs;
99 | }
100 |
101 | getResolvers() {
102 | const statements = ['Query', 'Mutation', 'Subscription'];
103 | const resolversDirPath = normalize('./appsync/resolvers')
104 | if (fs.existsSync(resolversDirPath)) {
105 | const files = fs.readdirSync(resolversDirPath)
106 | files.forEach(file => {
107 | // Example: Mutation.createChannel.response
108 | let args = file.split('.')
109 | let typeName: string = args[0];
110 | let name: string = args[1]
111 | let templateType = args[2] // request or response
112 | let filepath = normalize(`${resolversDirPath}/${file}`)
113 |
114 | if (statements.indexOf(typeName) >= 0 || (this.outputs.NONE && this.outputs.NONE[name])) {
115 | if (!this.resolvers[name]) {
116 | this.resolvers[name] = {
117 | typeName: typeName,
118 | fieldName: name,
119 | }
120 | }
121 |
122 | if (templateType === 'req') {
123 | this.resolvers[name]['requestMappingTemplate'] = filepath
124 | } else if (templateType === 'res') {
125 | this.resolvers[name]['responseMappingTemplate'] = filepath
126 | }
127 |
128 | } else { // This is a GSI
129 | if (!this.resolvers['gsi']) {
130 | this.resolvers['gsi'] = {}
131 | }
132 |
133 | let mapName = `${typeName}${name}`
134 | if (!this.resolvers['gsi'][mapName]) {
135 | this.resolvers['gsi'][mapName] = {
136 | typeName: typeName,
137 | fieldName: name,
138 | tableName: name.charAt(0).toUpperCase() + name.slice(1)
139 | }
140 | }
141 |
142 | if (templateType === 'req') {
143 | this.resolvers['gsi'][mapName]['requestMappingTemplate'] = filepath
144 | } else if (templateType === 'res') {
145 | this.resolvers['gsi'][mapName]['responseMappingTemplate'] = filepath
146 | }
147 | }
148 | })
149 | }
150 |
151 | return this.resolvers;
152 | }
153 |
154 | private writeSchema(schema: any) {
155 | if (!fs.existsSync(this.outputPath)) {
156 | fs.mkdirSync(this.outputPath);
157 | }
158 |
159 | fs.writeFileSync(`${this.outputPath}/schema.graphql`, schema)
160 | }
161 |
162 | private writeResolversToFile(resolvers: any) {
163 | if (!fs.existsSync(this.outputPath)) {
164 | fs.mkdirSync(this.outputPath);
165 | }
166 |
167 | const resolverFolderPath = normalize(this.outputPath + '/resolvers');
168 | if (fs.existsSync(resolverFolderPath)) {
169 | const files = fs.readdirSync(resolverFolderPath)
170 | files.forEach(file => fs.unlinkSync(resolverFolderPath + '/' + file))
171 | fs.rmdirSync(resolverFolderPath)
172 | }
173 |
174 | if (!fs.existsSync(resolverFolderPath)) {
175 | fs.mkdirSync(resolverFolderPath);
176 | }
177 |
178 | Object.keys(resolvers).forEach((key: any) => {
179 | const resolver = resolvers[key];
180 | const fileName = key.replace('.vtl', '');
181 | const resolverFilePath = normalize(`${resolverFolderPath}/${fileName}`);
182 | fs.writeFileSync(resolverFilePath, resolver);
183 | })
184 | }
185 |
186 | private loadConfigSync(projectDir: string): TransformConfig {
187 | // Initialize the config always with the latest version, other members are optional for now.
188 | let config = {
189 | Version: TRANSFORM_CURRENT_VERSION
190 | };
191 |
192 | try {
193 | const configPath = join(projectDir, TRANSFORM_CONFIG_FILE_NAME);
194 | const configExists = fs.existsSync(configPath);
195 | if (configExists) {
196 | const configStr = fs.readFileSync(configPath);
197 | config = JSON.parse(configStr.toString());
198 | }
199 | return config as TransformConfig;
200 | } catch (err) {
201 | return config;
202 | }
203 | }
204 | }
--------------------------------------------------------------------------------
/lib/transformer/transform.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": 5,
3 | "ResolverConfig": {
4 | "project": {
5 | "ConflictHandler": "OPTIMISTIC_CONCURRENCY",
6 | "ConflictDetection": "VERSION"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aws-cdk-appsync-transformer",
3 | "version": "1.63.0-rc.3",
4 | "description": "AWS Amplify inspired CDK construct for creating @directive based AppSync APIs",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "scripts": {
8 | "build": "jsii",
9 | "build:watch": "jsii -w",
10 | "package": "jsii-pacmak",
11 | "docgen": "jsii-docgen && mv API.md docs/",
12 | "cdk": "cdk",
13 | "check": "npm test && npm run lint",
14 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
15 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
16 | "postversion": "git push && git push --tags",
17 | "prepare": "npm run build",
18 | "prepublishOnly": "npm run check",
19 | "preversion": "npm run lint",
20 | "test": "jest",
21 | "version": "npm run format && git add -A src"
22 | },
23 | "jsii": {
24 | "outdir": "dist",
25 | "versionFormat": "short",
26 | "excludeTypescript": [
27 | "test"
28 | ],
29 | "targets": {
30 | "python": {
31 | "distName": "aws-cdk-appsync-transformer",
32 | "module": "aws_cdk_appsync_transformer"
33 | },
34 | "java": {
35 | "package": "io.github.kcwinner.AWSCDKAppSyncTransformer",
36 | "maven": {
37 | "groupId": "io.github.kcwinner",
38 | "artifactId": "AWSCDKAppSyncTransformer"
39 | }
40 | },
41 | "dotnet": {
42 | "namespace": "Kcwinner.AWSCDKAppSyncTransformer",
43 | "packageId": "Kcwinner.AWSCDKAppSyncTransformer"
44 | }
45 | }
46 | },
47 | "awscdkio": {
48 | "twitter": "KenWin0x539"
49 | },
50 | "stability": "experimental",
51 | "repository": {
52 | "type": "git",
53 | "url": "https://github.com/kcwinner/appsync-transformer-construct.git"
54 | },
55 | "keywords": [
56 | "aws",
57 | "cdk",
58 | "aws-cdk",
59 | "appsync",
60 | "amplify",
61 | "transformer"
62 | ],
63 | "license": "Apache-2.0",
64 | "author": {
65 | "name": "Ken Winner",
66 | "url": "https://github.com/kcwinner"
67 | },
68 | "devDependencies": {
69 | "@aws-cdk/assert": "1.63.0",
70 | "@types/jest": "^26.0.2",
71 | "@types/node": "^14.0.14",
72 | "@typescript-eslint/eslint-plugin": "^3.6.0",
73 | "@typescript-eslint/parser": "^3.6.0",
74 | "aws-cdk": "1.63.0",
75 | "eslint": "^7.4.0",
76 | "jest": "^26.4.2",
77 | "jsii": "^1.12.0",
78 | "jsii-docgen": "^1.4.27",
79 | "jsii-pacmak": "^1.12.0",
80 | "jsii-release": "^0.1.9",
81 | "ts-jest": "^26.4.0"
82 | },
83 | "peerDependencies": {
84 | "@aws-cdk/aws-appsync": "1.63.0",
85 | "@aws-cdk/aws-cognito": "1.63.0",
86 | "@aws-cdk/aws-dynamodb": "1.63.0",
87 | "@aws-cdk/aws-iam": "1.63.0",
88 | "@aws-cdk/aws-lambda": "1.63.0",
89 | "@aws-cdk/core": "1.63.0",
90 | "constructs": "^3.0.4"
91 | },
92 | "dependencies": {
93 | "@aws-cdk/aws-appsync": "1.63.0",
94 | "@aws-cdk/aws-cognito": "1.63.0",
95 | "@aws-cdk/aws-dynamodb": "1.63.0",
96 | "@aws-cdk/aws-iam": "1.63.0",
97 | "@aws-cdk/aws-lambda": "1.63.0",
98 | "@aws-cdk/core": "1.63.0",
99 | "@types/graphql": "^14.5.0",
100 | "cloudform-types": "^5.0.0",
101 | "graphql": "^14.6.0",
102 | "graphql-auth-transformer": "^6.18.1",
103 | "graphql-connection-transformer": "^4.18.1",
104 | "graphql-dynamodb-transformer": "^6.19.2",
105 | "graphql-function-transformer": "^2.3.9",
106 | "graphql-key-transformer": "^2.19.1",
107 | "graphql-mapping-template": "^4.13.4",
108 | "graphql-relational-schema-transformer": "^2.15.6",
109 | "graphql-transformer-common": "^4.17.1",
110 | "graphql-transformer-core": "^6.19.1",
111 | "graphql-versioned-transformer": "^4.15.9"
112 | },
113 | "bundledDependencies": [
114 | "@types/graphql",
115 | "cloudform-types",
116 | "graphql",
117 | "graphql-auth-transformer",
118 | "graphql-connection-transformer",
119 | "graphql-dynamodb-transformer",
120 | "graphql-function-transformer",
121 | "graphql-key-transformer",
122 | "graphql-mapping-template",
123 | "graphql-relational-schema-transformer",
124 | "graphql-transformer-common",
125 | "graphql-transformer-core",
126 | "graphql-versioned-transformer"
127 | ]
128 | }
129 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { App, Stack } from '@aws-cdk/core';
2 | import '@aws-cdk/assert/jest';
3 | import { AuthorizationType, AuthorizationConfig, UserPoolDefaultAction } from '@aws-cdk/aws-appsync';
4 | import { UserPool, UserPoolClient } from '@aws-cdk/aws-cognito';
5 |
6 | import { AppSyncTransformer } from '../lib/index';
7 |
8 | const apiKeyAuthorizationConfig: AuthorizationConfig = {
9 | defaultAuthorization: {
10 | authorizationType: AuthorizationType.API_KEY,
11 | apiKeyConfig: {
12 | description: "Auto generated API Key from construct",
13 | name: "dev",
14 | }
15 | }
16 | }
17 |
18 | test('GraphQL API W/ Defaults Created', () => {
19 | const mockApp = new App();
20 | const stack = new Stack(mockApp, 'testing-stack');
21 |
22 | const appSyncTransformer = new AppSyncTransformer(stack, 'test-transformer', {
23 | schemaPath: 'testSchema.graphql',
24 | authorizationConfig: apiKeyAuthorizationConfig
25 | });
26 |
27 | expect(stack).toHaveResource('AWS::CloudFormation::Stack');
28 | expect(appSyncTransformer.nestedAppsyncStack).toHaveResource('AWS::AppSync::GraphQLApi', {
29 | AuthenticationType: 'API_KEY'
30 | });
31 | });
32 |
33 | test('GraphQL API W/ Sync Created', () => {
34 | const mockApp = new App();
35 | const stack = new Stack(mockApp, 'testing-sync-stack');
36 |
37 | const appSyncTransformer = new AppSyncTransformer(stack, 'test-transformer', {
38 | schemaPath: 'testSchema.graphql',
39 | apiName: 'sync-api',
40 | authorizationConfig: apiKeyAuthorizationConfig,
41 | syncEnabled: true
42 | });
43 |
44 | expect(stack).toHaveResource('AWS::CloudFormation::Stack');
45 | expect(appSyncTransformer.nestedAppsyncStack).toHaveResource('AWS::AppSync::GraphQLApi', {
46 | AuthenticationType: 'API_KEY',
47 | Name: 'sync-api'
48 | });
49 | });
50 |
51 | test('GraphQL API W/ User Pool Auth Created', () => {
52 | const mockApp = new App();
53 | const stack = new Stack(mockApp, 'user-pool-auth-stack');
54 |
55 | const userPool = new UserPool(stack, 'test-userpool');
56 | const userPoolClient = new UserPoolClient(stack, 'test-userpool-client', {
57 | userPool: userPool
58 | })
59 |
60 | const appSyncTransformer = new AppSyncTransformer(stack, 'test-transformer', {
61 | schemaPath: 'testSchema.graphql',
62 | apiName: 'user-pool-auth-api',
63 | authorizationConfig: {
64 | defaultAuthorization: {
65 | authorizationType: AuthorizationType.USER_POOL,
66 | userPoolConfig: {
67 | userPool: userPool,
68 | appIdClientRegex: userPoolClient.userPoolClientId,
69 | defaultAction: UserPoolDefaultAction.ALLOW
70 | }
71 | }
72 | }
73 | });
74 |
75 | expect(stack).toHaveResource('AWS::CloudFormation::Stack');
76 | expect(appSyncTransformer.nestedAppsyncStack).toHaveResource('AWS::AppSync::GraphQLApi', {
77 | AuthenticationType: 'AMAZON_COGNITO_USER_POOLS',
78 | Name: 'user-pool-auth-api'
79 | });
80 | });
--------------------------------------------------------------------------------
/testSchema.graphql:
--------------------------------------------------------------------------------
1 | type Customer
2 | @model
3 | @auth(rules: [
4 | { allow: groups, groups: ["Admins"] },
5 | { allow: private, provider: iam, operations: [read, update] }
6 | ]) {
7 | id: ID!
8 | firstName: String!
9 | lastName: String!
10 | active: Boolean!
11 | address: String!
12 | }
13 |
14 | type Product
15 | @model
16 | @auth(rules: [
17 | { allow: groups, groups: ["Admins"] },
18 | { allow: public, provider: iam, operations: [read] }
19 | ]) {
20 | id: ID!
21 | name: String!
22 | description: String!
23 | price: String!
24 | active: Boolean!
25 | added: AWSDateTime!
26 | orders: [Order] @connection
27 | }
28 |
29 | # TODO: handle the subscription case when auth is not added - it doesn't create a subscription resolver
30 | type Order @model
31 | @key(fields: ["id", "productID"]) {
32 | id: ID!
33 | productID: ID!
34 | total: String!
35 | ordered: AWSDateTime!
36 | }
37 |
38 | type Blog @model {
39 | id: ID!
40 | name: String!
41 | posts: [Post] @connection(name: "BlogPosts")
42 | }
43 |
44 | type Post @model {
45 | id: ID!
46 | title: String!
47 | blog: Blog @connection(name: "BlogPosts")
48 | comments: [Comment] @connection(name: "PostComments")
49 | }
50 |
51 | type Comment @model {
52 | id: ID!
53 | content: String
54 | post: Post @connection(name: "PostComments")
55 | }
56 |
57 | # Demonstrate the FUNCTION resolvers
58 | type User @model(queries: null, mutations: null, subscriptions: null)
59 | @auth(rules: [
60 | { allow: groups, groups: ["Admins"] },
61 | { allow: owner, ownerField: "sub" },
62 | { allow: private, provider: iam, operations: [create, update] }
63 | ]) {
64 | id: ID!
65 | enabled: Boolean!
66 | status: String!
67 | email: String!
68 | name: String!
69 | email_verified: String
70 | phone_number: String
71 | phone_number_verified: String
72 | }
73 |
74 | type UserConnection {
75 | items: [User]
76 | }
77 |
78 | input CreateUserInput {
79 | email: String!
80 | name: String!
81 | }
82 |
83 | input UpdateUserInput {
84 | id: ID!
85 | email: String
86 | name: String
87 | number: String
88 | }
89 |
90 | # Demonstrate the FUNCTION resolvers
91 | type Query {
92 | listUsers: UserConnection @function(name: "currently-unused")
93 | getUser(id: ID!): User @function(name: "currently-unused")
94 | }
95 |
96 | type Mutation {
97 | createUser(input: CreateUserInput!): User @function(name: "currently-unused")
98 | updateUser(input: UpdateUserInput!): User @function(name: "currently-unused")
99 | }
--------------------------------------------------------------------------------