├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── .prettierrc.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── dangerfile.ts ├── designs ├── initial.md └── meetings │ └── 17-12-14.md ├── docs ├── README.md └── rest.md ├── examples ├── advanced │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── SearchShow.js │ │ ├── index.css │ │ └── index.js ├── simple │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── Person.js │ │ ├── index.css │ │ └── index.js └── typescript │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── Repo.tsx │ ├── RepoSearch.tsx │ ├── index.css │ └── index.tsx │ └── tsconfig.json ├── jest.config.js ├── package.json ├── renovate.json ├── rollup.config.js ├── schema.graphql ├── scripts ├── deploy.sh ├── docs_check.sh ├── docs_pull.sh ├── docs_push.sh └── jest.js ├── src ├── __tests__ │ └── restLink.ts ├── index.ts ├── restLink.ts └── utils │ └── graphql.ts ├── tsconfig.json └── tsconfig.tests.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | secops: apollo/circleci-secops-orb@2.0.1 5 | 6 | workflows: 7 | security-scans: 8 | jobs: 9 | - secops/gitleaks: 10 | context: 11 | - platform-docker-ro 12 | - github-orb 13 | - secops-oidc 14 | git-base-revision: <<#pipeline.git.base_revision>><><> 15 | git-revision: << pipeline.git.revision >> 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | 49 | # lock files 50 | yarn.lock 51 | package-lock.json 52 | 53 | # Compiled 54 | dist 55 | lib 56 | 57 | .idea 58 | .vscode 59 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/tests/ 2 | src/ 3 | tests/ 4 | .travis.yml 5 | tsconfig.json 6 | tslint.json 7 | typings.d.ts -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: all 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '16' 4 | 5 | script: 6 | # - npm run danger (disable until it's fixed) 7 | 8 | # using jest --coverage also runs the tests so this will cut down CI time 9 | - npm run coverage 10 | 11 | # run coverage and file size checks 12 | - npm run coverage:upload || true #ignore failures 13 | 14 | # make sure files don't get too large 15 | - npm run filesize 16 | 17 | # make sure there are no type errors 18 | - npm run check-types 19 | 20 | # Allow Travis tests to run in containers. 21 | sudo: false 22 | 23 | notifications: 24 | email: false 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## Versions 4 | 5 | ### v0.next 6 | 7 | ### v0.9.0 8 | 9 | * Feature: Pass context to typePatcher to make it possible to extract args from the URL for use when patching [#260](https://github.com/apollographql/apollo-link-rest/pull/260) [#261](https://github.com/apollographql/apollo-link-rest/pull/261) 10 | * Feature: Allow per-request field-name normalization for symmetry with de-normalization [#253](https://github.com/apollographql/apollo-link-rest/pull/253) 11 | * Improvement: Use globalThis instead of global [#293](https://github.com/apollographql/apollo-link-rest/pull/293) 12 | * Fix: fieldNameNormalizer mangling ArrayBuffer/Blob types [#247](https://github.com/apollographql/apollo-link-rest/pull/247) 13 | * Drop dependency on `graphql-anywhere`! [#301](https://github.com/apollographql/apollo-link-rest/pull/301) 14 | 15 | ### v0.8.0 16 | 17 | In beta for a unreasonably long time, this release has been stable (in-beta) for 2 years now!! It's about time we officially tag the stable version. 18 | 19 | The main breaking change is that you need to be running Apollo Client >= 3. 20 | 21 | A list of some specific relevant PRs: 22 | 23 | * [#241](https://github.com/apollographql/apollo-link-rest/pull/241) 24 | * [#239](https://github.com/apollographql/apollo-link-rest/pull/239) 25 | * [#228](https://github.com/apollographql/apollo-link-rest/pull/228) 26 | * [#209](https://github.com/apollographql/apollo-link-rest/pull/209) 27 | 28 | ### v0.7.3 29 | 30 | * Fix: Nested `@rest(…)` calls with nested `@export(as:…)` directives should keep their contexts distinct in order to work. [#204](https://github.com/apollographql/apollo-link-rest/pull/204) 31 | 32 | ### v0.7.2 33 | 34 | * Fix: FileList/File aren't available in react-native causing crashes. [#200](https://github.com/apollographql/apollo-link-rest/pull/200) 35 | 36 | ### v0.7.1 37 | 38 | * Fix: Duplicated Content Type Header [#188](https://github.com/apollographql/apollo-link-rest/pull/188) 39 | * Fix: FileList Support [#183](https://github.com/apollographql/apollo-link-rest/pull/183) 40 | * Fix: Default Empty object when creating headers [#178](https://github.com/apollographql/apollo-link-rest/pull/178) 41 | * Body-containing Queries [#173](https://github.com/apollographql/apollo-link-rest/pull/173) 42 | 43 | ### v0.7.0 - Breaking! 44 | 45 | #### Breaking changes around `responseTransformer!` 46 | 47 | In this [PR #165](https://github.com/apollographql/apollo-link-rest/pull/165), we realized that the `responseTransformer` feature added last release wasn't broad enough, `responseTransformer`s now receive the raw response stream instead of just the `json()`-promise. 48 | 49 | Code which relies on this feature will break, however the fix should be very simple: 50 | 51 | Either the responseTransformer function is made `async` and to `await response.json()`, *or* if this syntax is not available, the existing code needs to be wrapped in `response.json().then(data => {/* existing implementation */})`. 52 | 53 | #### Other Changes 54 | 55 | * Remove restriction that only allows request bodies to be built for Mutation operations. [#154](https://github.com/apollographql/apollo-link-rest/issues/154) & [#173](https://github.com/apollographql/apollo-link-rest/pull/173) 56 | * Fix code sandbox examples [#177](https://github.com/apollographql/apollo-link-rest/pull/177) 57 | * Bug-fix: default to empty headers instead of undefined for IE [#178](https://github.com/apollographql/apollo-link-rest/pull/178) 58 | * Various docs typo fixes 59 | 60 | 61 | ### v0.6.0 62 | 63 | * Feature: responseTransformers allow you to restructure & erase "wrapper" objects from your responses. [#146](https://github.com/apollographql/apollo-link-rest/pull/146) 64 | * Tweaks to config for prettier [#153](https://github.com/apollographql/apollo-link-rest/pull/153) & jest [#158](https://github.com/apollographql/apollo-link-rest/pull/158) 65 | * Tests for No-Content responses [#157](https://github.com/apollographql/apollo-link-rest/pull/157) & [#161](https://github.com/apollographql/apollo-link-rest/pull/161) 66 | * Bundle Size-Limit Increased [#162](https://github.com/apollographql/apollo-link-rest/pull/162) 67 | * Restructure Code for preferring `await` over Promise-chains [#159](https://github.com/apollographql/apollo-link-rest/pull/159) 68 | 69 | ### v0.5.0 70 | 71 | * Breaking Change: 404s now no longer throw an error! It's just null data! [#142](https://github.com/apollographql/apollo-link-rest/pull/142) 72 | * Default Accept header if no header configured for `Accept:` [#143](https://github.com/apollographql/apollo-link-rest/pull/143) 73 | * Improve/enable Support for Nested Queries from different apollo-links [#138](https://github.com/apollographql/apollo-link-rest/pull/138) 74 | * Remove Restriction that Mutation must not contain GET queries & vice versa [#140](https://github.com/apollographql/apollo-link-rest/issues/140) 75 | 76 | ### v0.4.3, v0.4.4 77 | 78 | * Expose an internal helper class (PathBuilder) for experimentation 79 | 80 | ### v0.4.2 81 | 82 | * Fix: Bad regexp causes path-replacements with multiple replacement slots to fail [#135](https://github.com/apollographql/apollo-link-rest/issues/135) 83 | 84 | ### v0.4.1 85 | 86 | * Fix: Correctly slicing nested key-paths [#134](https://github.com/apollographql/apollo-link-rest/issues/134) 87 | * Fix: Improve Types for ServerError [#133](https://github.com/apollographql/apollo-link-rest/pull/133) 88 | 89 | ### v0.4.0 90 | 91 | Breaking changes around `path`-variable replacement and `pathBuilder` (previously undocumented, [#132](https://github.com/apollographql/apollo-link-rest/issues/132)). 92 | 93 | * Breaking Change: paths now have a new style for variable replacement. (Old style is marked as deprecated, but will still work until v0.5.0). The migration should be easy in most cases `/path/:foo` => `/path/{args.foo}` 94 | * Breaking Change: `pathBuilder` signature changes to give them access to context & other data [#131](https://github.com/apollographql/apollo-link-rest/issues/131) and support optional Values [#130](https://github.com/apollographql/apollo-link-rest/issues/130) 95 | * Breaking Change: `bodyBuilder` signature changes to give them access to context & other data (for consistency with `pathBuilder`) 96 | * Fix/Feature: Queries that fetch Scalar values or Arrays of scalar values should now work! [#129](https://github.com/apollographql/apollo-link-rest/issues/129) 97 | 98 | ### v0.3.1 99 | 100 | * Fix: Fetch Response bodies can only be "read" once after which they throw "Already Read" -- this prevented us from properly speculatively parsing the error bodies outside of a test environment. [#122](https://github.com/apollographql/apollo-link-rest/issues/122) 101 | * Fix: Some browsers explode when you send null to them! [#121](https://github.com/apollographql/apollo-link-rest/issues/121#issuecomment-396049677) 102 | 103 | ### v0.3.0 104 | 105 | * Feature: Expose Headers from REST responses to the apollo-link chain via context. [#106](https://github.com/apollographql/apollo-link-rest/issues/106) 106 | * Feature: Expose HTTP-error REST responses as JSON if available! [#94](https://github.com/apollographql/apollo-link-rest/issues/94) 107 | * Feature: Add `@type(name: )` as an alternative, lighter-weight system for tagging Nested objects with \_\_typenames! [#72](https://github.com/apollographql/apollo-link-rest/issues/72) 108 | * Feature: Support "No-Content" responses! [#107](https://github.com/apollographql/apollo-link-rest/pull/107) [#111](https://github.com/apollographql/apollo-link-rest/pull/111) 109 | * Feature: Support serializing the body of REST calls with formats other than JSON [#103](https://github.com/apollographql/apollo-link-rest/pull/103) 110 | * Fix: Bundle-size / Tree Shaking issues [#99](https://github.com/apollographql/apollo-link-rest/issues/99) 111 | * Fix: Dependency tweaks to prevent multiple versions of deps [#105](https://github.com/apollographql/apollo-link-rest/issues/105) 112 | * Fix: GraphQL Nested Aliases - [#113](https://github.com/apollographql/apollo-link-rest/pull/113) [#7](https://github.com/apollographql/apollo-link-rest/issues/7) 113 | 114 | ### v0.2.4 115 | 116 | * Enable JSDoc comments for TypeScript! 117 | * Add in-repo copy of docs so PRs can make changes to docs in sync with implementation changes. 118 | * Fixed a bug with recursive type-patching around arrays. 119 | * Fixed a bug in default URI assignment! [#91](https://github.com/apollographql/apollo-link-rest/pull/91) 120 | 121 | ### v0.2.3 122 | 123 | * Fix: react-native: Android boolean responses being iterated by fieldNameNormalizer throws an error [#89](https://github.com/apollographql/apollo-link-rest/issues/89) 124 | 125 | ### v0.2.2 126 | 127 | * Fix: Queries with Arrays & omitted fields would treat those fields as required (and fail) [#85](https://github.com/apollographql/apollo-link-rest/issues/85) 128 | 129 | ### v0.2.1 130 | 131 | * Fix: Query throws an error when path-parameter is falsy [#82](https://github.com/apollographql/apollo-link-rest/issues/82) 132 | * Fix: Concurrency bug when multiple requests are in flight and both use `@export(as:)` [#81](https://github.com/apollographql/apollo-link-rest/issues/81) 133 | * Fix: fieldNameNormalizer/fieldNameDenormalizer should now be working! [#80](https://github.com/apollographql/apollo-link-rest/issues/80) 134 | * Improvement: Jest should now report code-coverage correctly for Unit Tests on PRs! 135 | 136 | ### v0.2.0 137 | 138 | * Feature: Support Handling Non-success HTTP Status Codes 139 | * Feature: Dynamic Paths & Query building using `pathBuilder` 140 | * Improvement: Sourcemaps should now be more TypeScript aware (via rollup changes) see [#76](https://github.com/apollographql/apollo-link-rest/issues/76) for more up-to-date info. 141 | 142 | ### v0.1.0 143 | 144 | Dropping the alpha tag, but keeping the pre-1.0 nature of this project! 145 | 146 | Recent changes: 147 | 148 | * Fix/Feature: Ability to have deeply nested responses that have \_\_typename set correctly. See `typePatcher` for more details 149 | * Fix: Real-world mutations need their bodies serialized. Mock-fetch allowed incorrect tests to be written. thanks @fabien0102 150 | * Feature: Bodies for mutations can be custom-built. 151 | 152 | ### v0.0.1-alpha.1 153 | 154 | ### v0.0.1-alpha.0 155 | 156 | * First publish 157 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Apollo Contributor Guide 2 | 3 | Excited about Apollo and want to make it better? We’re excited too! 4 | 5 | Apollo is a community of developers just like you, striving to create the best tools and libraries around GraphQL. We welcome anyone who wants to contribute or provide constructive feedback, no matter the age or level of experience. If you want to help but don't know where to start, let us know, and we'll find something for you. 6 | 7 | Oh, and if you haven't already, sign up for the [Apollo Slack](http://www.apollodata.com/#slack). 8 | 9 | Here are some ways to contribute to the project, from easiest to most difficult: 10 | 11 | * [Reporting bugs](#reporting-bugs) 12 | * [Improving the documentation](#improving-the-documentation) 13 | * [Responding to issues](#responding-to-issues) 14 | * [Small bug fixes](#small-bug-fixes) 15 | * [Suggesting features](#feature-requests) 16 | * [Big pull requests](#big-prs) 17 | 18 | ## Issues 19 | 20 | ### Reporting bugs 21 | 22 | If you encounter a bug, please file an issue on GitHub via the repository of the sub-project you think contains the bug. If an issue you have is already reported, please add additional information or add a 👍 reaction to indicate your agreement. 23 | 24 | While we will try to be as helpful as we can on any issue reported, please include the following to maximize the chances of a quick fix: 25 | 26 | 1. **Intended outcome:** What you were trying to accomplish when the bug occurred, and as much code as possible related to the source of the problem. 27 | 2. **Actual outcome:** A description of what actually happened, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. Please avoid non-specific phrases like “didn’t work” or “broke”. 28 | 3. **How to reproduce the issue:** Instructions for how the issue can be reproduced by a maintainer or contributor. Be as specific as possible, and only mention what is necessary to reproduce the bug. If possible, build a reproduction with our [error template](https://github.com/apollographql/react-apollo-error-template) to isolate the exact circumstances in which the bug occurs. Avoid speculation over what the cause might be. 29 | 30 | Creating a good reproduction really helps contributors investigate and resolve your issue quickly. In many cases, the act of creating a minimal reproduction illuminates that the source of the bug was somewhere outside the library in question, saving time and effort for everyone. 31 | 32 | ### Improving the documentation 33 | 34 | Improving the documentation, examples, and other open source content can be the easiest way to contribute to the library. If you see a piece of content that can be better, open a PR with an improvement, no matter how small! If you would like to suggest a big change or major rewrite, we’d love to hear your ideas but please open an issue for discussion before writing the PR. 35 | 36 | ### Responding to issues 37 | 38 | In addition to reporting issues, a great way to contribute to Apollo is to respond to other peoples' issues and try to identify the problem or help them work around it. If you’re interested in taking a more active role in this process, please go ahead and respond to issues. And don't forget to say "Hi" on Apollo Slack! 39 | 40 | ### Small bug fixes 41 | 42 | For a small bug fix change (less than 20 lines of code changed), feel free to open a pull request. We’ll try to merge it as fast as possible and ideally publish a new release on the same day. The only requirement is, make sure you also add a test that verifies the bug you are trying to fix. 43 | 44 | ### Suggesting features 45 | 46 | Most of the features in Apollo came from suggestions by you, the community! We welcome any ideas about how to make Apollo better for your use case. Unless there is overwhelming demand for a feature, it might not get implemented immediately, but please include as much information as possible that will help people have a discussion about your proposal: 47 | 48 | 1. **Use case:** What are you trying to accomplish, in specific terms? Often, there might already be a good way to do what you need and a new feature is unnecessary, but it’s hard to know without information about the specific use case. 49 | 2. **Could this be a plugin?** In many cases, a feature might be too niche to be included in the core of a library, and is better implemented as a companion package. If there isn’t a way to extend the library to do what you want, could we add additional plugin APIs? It’s important to make the case for why a feature should be part of the core functionality of the library. 50 | 3. **Is there a workaround?** Is this a more convenient way to do something that is already possible, or is there some blocker that makes a workaround unfeasible? 51 | 52 | Feature requests will be labeled as such, and we encourage using GitHub issues as a place to discuss new features and possible implementation designs. Please refrain from submitting a pull request to implement a proposed feature until there is consensus that it should be included. This way, you can avoid putting in work that can’t be merged in. 53 | 54 | Once there is a consensus on the need for a new feature, proceed as listed below under “Big PRs”. 55 | 56 | ## Big PRs 57 | 58 | This includes: 59 | 60 | - Big bug fixes 61 | - New features 62 | 63 | For significant changes to a repository, it’s important to settle on a design before starting on the implementation. This way, we can make sure that major improvements get the care and attention they deserve. Since big changes can be risky and might not always get merged, it’s good to reduce the amount of possible wasted effort by agreeing on an implementation design/plan first. 64 | 65 | 1. **Open an issue.** Open an issue about your bug or feature, as described above. 66 | 2. **Reach consensus.** Some contributors and community members should reach an agreement that this feature or bug is important, and that someone should work on implementing or fixing it. 67 | 3. **Agree on intended behavior.** On the issue, reach an agreement about the desired behavior. In the case of a bug fix, it should be clear what it means for the bug to be fixed, and in the case of a feature, it should be clear what it will be like for developers to use the new feature. 68 | 4. **Agree on implementation plan.** Write a plan for how this feature or bug fix should be implemented. What modules need to be added or rewritten? Should this be one pull request or multiple incremental improvements? Who is going to do each part? 69 | 5. **Submit PR.** In the case where multiple dependent patches need to be made to implement the change, only submit one at a time. Otherwise, the others might get stale while the first is reviewed and merged. Make sure to avoid “while we’re here” type changes - if something isn’t relevant to the improvement at hand, it should be in a separate PR; this especially includes code style changes of unrelated code. 70 | 6. **Review.** At least one core contributor should sign off on the change before it’s merged. Look at the “code review” section below to learn about factors are important in the code review. If you want to expedite the code being merged, try to review your own code first! 71 | 7. **Merge and release!** 72 | 73 | ### Code review guidelines 74 | 75 | It’s important that every piece of code in Apollo packages is reviewed by at least one core contributor familiar with that codebase. Here are some things we look for: 76 | 77 | 1. **Required CI checks pass.** This is a prerequisite for the review, and it is the PR author's responsibility. As long as the tests don’t pass, the PR won't get reviewed. 78 | 2. **Simplicity.** Is this the simplest way to achieve the intended goal? If there are too many files, redundant functions, or complex lines of code, suggest a simpler way to do the same thing. In particular, avoid implementing an overly general solution when a simple, small, and pragmatic fix will do. 79 | 3. **Testing.** Do the tests ensure this code won’t break when other stuff changes around it? When it does break, will the tests added help us identify which part of the library has the problem? Did we cover an appropriate set of edge cases? Look at the test coverage report if there is one. Are all significant code paths in the new code exercised at least once? 80 | 4. **No unnecessary or unrelated changes.** PRs shouldn’t come with random formatting changes, especially in unrelated parts of the code. If there is some refactoring that needs to be done, it should be in a separate PR from a bug fix or feature, if possible. 81 | 5. **Code has appropriate comments.** Code should be commented, or written in a clear “self-documenting” way. 82 | 6. **Idiomatic use of the language.** In TypeScript, make sure the typings are specific and correct. In ES2015, make sure to use imports rather than require and const instead of var, etc. Ideally a linter enforces a lot of this, but use your common sense and follow the style of the surrounding code. 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - 2017 Meteor Development Group, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: REST Link 3 | --- 4 | 5 | ## ⚠️ This library is under active development ⚠️ 6 | 7 | > The [Apollo Link Rest](https://github.com/apollographql/apollo-link-rest) library is maintained by Apollo community members and not an Apollo GraphQL maintained library. For information on progress check out [the issues](https://github.com/apollographql/apollo-link-rest/issues) or [the design](./designs/initial.md). We would love your help with writing docs, testing, anything! We would love for you, yes you, to be a part of the Apollo community! 8 | 9 | ## Purpose 10 | An Apollo Link to easily try out GraphQL without a full server. It can be used to prototype, with third-party services that don't have a GraphQL endpoint or in a transition from REST to GraphQL. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install apollo-link-rest @apollo/client graphql qs --save 16 | or 17 | yarn add apollo-link-rest @apollo/client graphql qs 18 | ``` 19 | 20 | `@apollo/client`, `graphql`, and `qs` are peer dependencies needed by `apollo-link-rest`. 21 | 22 | ## Usage 23 | 24 | ### Basics 25 | 26 | ```js 27 | import { RestLink } from "apollo-link-rest"; 28 | // Other necessary imports... 29 | 30 | // Create a RestLink for the REST API 31 | // If you are using multiple link types, restLink should go before httpLink, 32 | // as httpLink will swallow any calls that should be routed through rest! 33 | const restLink = new RestLink({ 34 | uri: 'https://swapi.co/api/', 35 | }); 36 | 37 | // Configure the ApolloClient with the default cache and RestLink 38 | const client = new ApolloClient({ 39 | link: restLink, 40 | cache: new InMemoryCache(), 41 | }); 42 | 43 | // A simple query to retrieve data about the first person 44 | const query = gql` 45 | query luke { 46 | person @rest(type: "Person", path: "people/1/") { 47 | name 48 | } 49 | } 50 | `; 51 | 52 | // Invoke the query and log the person's name 53 | client.query({ query }).then(response => { 54 | console.log(response.data.person.name); 55 | }); 56 | ``` 57 | 58 | [![Edit Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/apollo-link-rest/tree/master/examples/simple) 59 | 60 | ### Apollo Client & React Apollo 61 | 62 | For an example of using REST Link with Apollo Client and React Apollo view this CodeSandbox: 63 | 64 | [![Edit Advanced Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/apollo-link-rest/tree/master/examples/advanced) 65 | 66 | ### TypeScript 67 | 68 | For an example of using REST Link with Apollo Client, React Apollo and TypeScript view this CodeSandbox: 69 | 70 | [![Edit TypeScript Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/apollo-link-rest/tree/master/examples/typescript) 71 | 72 | ## Options 73 | 74 | REST Link takes an object with some options on it to customize the behavior of the link. The options you can pass are outlined below: 75 | 76 | - `uri`: the URI key is a string endpoint (optional when `endpoints` provides a default) 77 | - `endpoints`: root endpoint (uri) to apply paths to or a map of endpoints 78 | - `customFetch`: a custom `fetch` to handle REST calls 79 | - `headers`: an object representing values to be sent as headers on the request 80 | - `credentials`: a string representing the credentials policy you want for the fetch call 81 | - `fieldNameNormalizer`: function that takes the response field name and converts it into a GraphQL compliant name 82 | 83 | ## Context 84 | 85 | REST Link uses the `headers` field on the context to allow passing headers to the HTTP request. It also supports the `credentials` field for defining credentials policy. 86 | 87 | - `headers`: an object representing values to be sent as headers on the request 88 | - `credentials`: a string representing the credentials policy you want for the fetch call 89 | 90 | ## Documentation 91 | 92 | For a complete `apollo-link-rest` reference visit the documentation website at: https://www.apollographql.com/docs/link/links/rest.html 93 | 94 | ## Contributing 95 | 96 | This project uses TypeScript to bring static types to JavaScript and uses Jest for testing. To get started, clone the repo and run the following commands: 97 | 98 | ```bash 99 | npm install # or `yarn` 100 | 101 | npm test # or `yarn test` to run tests 102 | npm test -- --watch # run tests in watch mode 103 | 104 | npm run check-types # or `yarn check-types` to check TypeScript types 105 | ``` 106 | 107 | To run the library locally in another project, you can do the following: 108 | 109 | ```bash 110 | npm link 111 | 112 | # in the project you want to run this in 113 | npm link apollo-link-rest 114 | ``` 115 | 116 | ## Related Libraries 117 | 118 | - [JSON API Link](https://github.com/Rsullivan00/apollo-link-json-api/) provides 119 | tooling for using GraphQL with JSON API compliant APIs. 120 | - [apollo-type-patcher](https://github.com/mpgon/apollo-type-patcher) declarative type definitions for your REST API with zero dependencies. 121 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | parsers: 3 | javascript: 4 | enable_partials: yes 5 | status: 6 | project: 7 | default: 8 | target: "83%" 9 | patch: 10 | enabled: false 11 | -------------------------------------------------------------------------------- /dangerfile.ts: -------------------------------------------------------------------------------- 1 | // Removed import 2 | import danger from 'danger'; 3 | import { includes } from 'lodash'; 4 | import * as fs from 'fs'; 5 | 6 | // Setup 7 | const pr = danger.github.pr; 8 | const commits = danger.github.commits; 9 | const modified = danger.git.modified_files; 10 | const bodyAndTitle = (pr.body + pr.title).toLowerCase(); 11 | 12 | // Custom modifiers for people submitting PRs to be able to say "skip this" 13 | const trivialPR = bodyAndTitle.includes('trivial'); 14 | const acceptedNoTests = bodyAndTitle.includes('skip new tests'); 15 | 16 | const typescriptOnly = (file: string) => includes(file, '.ts'); 17 | const filesOnly = (file: string) => 18 | fs.existsSync(file) && fs.lstatSync(file).isFile(); 19 | 20 | // Custom subsets of known files 21 | const modifiedAppFiles = modified 22 | .filter(p => includes(p, 'src') || includes(p, '__tests__')) 23 | .filter(p => filesOnly(p) && typescriptOnly(p)); 24 | 25 | // Takes a list of file paths, and converts it into clickable links 26 | const linkableFiles = paths => { 27 | const repoURL = danger.github.pr.head.repo.html_url; 28 | const ref = danger.github.pr.head.ref; 29 | const links = paths.map(path => { 30 | return createLink(`${repoURL}/blob/${ref}/${path}`, path); 31 | }); 32 | return toSentence(links); 33 | }; 34 | 35 | // ["1", "2", "3"] to "1, 2 and 3" 36 | const toSentence = (array: Array): string => { 37 | if (array.length === 1) { 38 | return array[0]; 39 | } 40 | return array.slice(0, array.length - 1).join(', ') + ' and ' + array.pop(); 41 | }; 42 | 43 | // ("/href/thing", "name") to "name" 44 | const createLink = (href: string, text: string): string => 45 | `${text}`; 46 | 47 | // Raise about missing code inside files 48 | const raiseIssueAboutPaths = ( 49 | type: Function, 50 | paths: string[], 51 | codeToInclude: string, 52 | ) => { 53 | if (paths.length > 0) { 54 | const files = linkableFiles(paths); 55 | const strict = '' + codeToInclude + ''; 56 | type(`Please ensure that ${strict} is enabled on: ${files}`); 57 | } 58 | }; 59 | 60 | const authors = commits.map(x => x.author.login); 61 | const isBot = authors.some( 62 | x => ['greenkeeper', 'renovate', 'dependabot'].indexOf(x) > -1, 63 | ); 64 | 65 | // Rules 66 | if (!isBot) { 67 | // make sure someone else reviews these changes 68 | // const someoneAssigned = danger.github.pr.assignee; 69 | // if (someoneAssigned === null) { 70 | // warn( 71 | // 'Please assign someone to merge this PR, and optionally include people who should review.' 72 | // ); 73 | // } 74 | 75 | // When there are app-changes and it's not a PR marked as trivial, expect 76 | // there to be CHANGELOG changes. 77 | // const changelogChanges = modified.some(x => x.indexOf('CHANGELOG') > -1); 78 | // if (modifiedAppFiles.length > 0 && !trivialPR && !changelogChanges) { 79 | // fail('No CHANGELOG added.'); 80 | // } 81 | 82 | // No PR is too small to warrant a paragraph or two of summary 83 | if (pr.body.length === 0) { 84 | fail('Please add a description to your PR.'); 85 | } 86 | 87 | const hasAppChanges = modifiedAppFiles.length > 0; 88 | 89 | const testChanges = modifiedAppFiles.filter(filepath => 90 | filepath.includes('test'), 91 | ); 92 | const hasTestChanges = testChanges.length > 0; 93 | 94 | // Warn when there is a big PR 95 | const bigPRThreshold = 500; 96 | if ( 97 | danger.github.pr.additions + danger.github.pr.deletions > 98 | bigPRThreshold 99 | ) { 100 | warn(':exclamation: Big PR'); 101 | } 102 | 103 | // Warn if there are library changes, but not tests 104 | if (hasAppChanges && !hasTestChanges) { 105 | warn( 106 | "There are library changes, but not tests. That's OK as long as you're refactoring existing code", 107 | ); 108 | } 109 | 110 | // Be careful of leaving testing shortcuts in the codebase 111 | const onlyTestFiles = testChanges.filter(x => { 112 | const content = fs.readFileSync(x).toString(); 113 | return ( 114 | content.includes('it.only') || 115 | content.includes('describe.only') || 116 | content.includes('fdescribe') || 117 | content.includes('fit(') 118 | ); 119 | }); 120 | raiseIssueAboutPaths(fail, onlyTestFiles, 'an `only` was left in the test'); 121 | 122 | // Politely ask for their name in the authors file 123 | message('Please add your name and email to the AUTHORS file (optional)'); 124 | message( 125 | 'If this was a change that affects the external API, please update the docs and post a link to the PR in the discussion', 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /designs/initial.md: -------------------------------------------------------------------------------- 1 | ## `@rest` directive 2 | 3 | ### Arguments 4 | 5 | - path: path to rest endpoint. This could be a path or a full url. If a path, add to the endpoint given on link creation or from the context is concatenated to it. 6 | - params: a map of variables to url params 7 | - method: the HTTP method to send the request via (i.e GET, PUT, POST) 8 | - type: The GraphQL type this will return 9 | - endpoint: which endpoint (if using a map of endpoints) to use for the request 10 | 11 | ### Notes 12 | 13 | It's important that the rest directive could be used at any depth in a query, but once it is used, nothing nested in it can be graphql data, it has to be from the rest link or other resource (like a @client directive) 14 | 15 | ## `@export` directive 16 | 17 | The export directive re-exposes a field for use in a later (nested) query. An example use-case would be getting a list of users, and hitting a different endpoint to fetch more data using the exported field in the REST query args. 18 | 19 | ### Arguments 20 | - as: the string name to create this as a variable to be used down the selection set 21 | 22 | ### Notes 23 | 24 | These are the same semantics that will be supported on the server, but when used in a rest link you can use the exported variables for futher calls (i.e. waterfall requests from nested fields) 25 | 26 | ```js 27 | const QUERY = gql` 28 | query RestData($email: String!) { 29 | users @rest(path: '/users/email/:email', params: { email: $email }, method: 'GET', type: 'User') { 30 | id @export(as: "id") 31 | firstName 32 | lastName 33 | friends @rest(path: '/friends/:id', params: { id: $id }, type: '[User]') { 34 | firstName 35 | lastName 36 | } 37 | } 38 | } 39 | `; 40 | ``` 41 | 42 | 43 | ## `createRestLink` 44 | 45 | ### Arguments 46 | 47 | - `fetch`: an optional implementation of `fetch` (see the http-link for api / warnings). Will use global if found 48 | - `fieldNameNormalizer`: a function that takes the response field name and turns into a GraphQL compliant name,for instance "MyFieldName:IsGreat" => myFieldNameIsGreat 49 | - `endpoint`: a root endpoint (uri) to apply paths to: i.e. http[s]://api.example.com/v1 or a map of endpoints with a key to choose in the directive 50 | - `batch`: a boolean to batch possible calls together (not initial version requirement!) 51 | - `headers`: an object representing values to be sent as headers on each request 52 | - `credentials`: a string representing the credentials policy you want for the fetch call 53 | - `fetchOptions`: any overrides of the fetch options argument to pass to the fetch call 54 | 55 | ### Notes 56 | 57 | It would be great to support batching of calls to /users if they are sent at the same time (i.e. dataloader) but definitely not something for the first round. Most of the tools around directives could be pulled from the apollo-link-state project to do the same work. For a mixed graphql + rest query, rest should be executed second as it may be nested and need the data (for instance with @export usage) 58 | 59 | ```js 60 | const link = createRestLink({ 61 | endpoint: "https://api.example.com/v1", 62 | // endpoint: { "version1": "https://api.example.com/v1", "version2": "https://api.example.com/v2" }, 63 | fetch: nodeFetch, 64 | fieldNameNormalizer: name => camelCase(name), 65 | // batch: false 66 | }); 67 | ``` 68 | -------------------------------------------------------------------------------- /designs/meetings/17-12-14.md: -------------------------------------------------------------------------------- 1 | # Launch meeting 2 | 3 | ## Attendees 4 | - Peggy Rayzis (Apollo) 5 | - Victor Sabatier (Reactivic) 6 | - Frederic Barthelemy (TaskRabbit) 7 | 8 | ## Notes 9 | - Features left to complete 10 | - https://github.com/apollographql/apollo-link-rest/issues/3 11 | - Action: Close this issue and open up a new one for mixed directives since it's not a release blocker 12 | - Launch blockers 13 | - Documentation 14 | - Apollo Link docs & Apollo Client docs 15 | - Symlink Apollo Link docs page to README 16 | - Action: Peggy will open up issues w/ links to the process so Victor & Frederic can collaborate 17 | - https://github.com/apollographql/apollo-link-rest/issues/22 18 | - Example app 19 | - Next week, Victor & Frederic will collaborate on this 20 | - Start w/ a simple example: REST endpoint 21 | - Host on CodeSandbox 22 | - Git integration allows you to store the examples in the REST link repo 23 | - https://github.com/apollographql/apollo-link-rest/issues/23 24 | - Blog post 25 | - Frederic & Victor will divide up the sections 26 | - Target date: Send to Apollo team the week between Christmas & New Years 27 | - https://github.com/apollographql/apollo-link-rest/issues/21 28 | - Launch plan: Target date (first week of the new year) 29 | - Future (v1.0) 30 | - What's our (REST-response) typechecking story? 31 | - Start thinking of best practices (query components vs. HOC) 32 |    - Query components are very tempting for people trying to migrate, but schema-stitching & GraphQL fragments are valuable GraphQL features that a naïve implementation of query-components would fail to support. We don't want people "learning" GraphQL using Apollo-link-rest, but avoiding these features. [Frederic will be prototyping some query-components with fragment support in the next few weeks] 33 | 34 | - Organisation 35 | - Who is willing to maintain/contribute on this project ? 36 | - Code Process 37 | - Continue iterating quickly, with PRs opened for major changes, but don't let one's self get blocked. -- It's fine if a PR is only open for a few hours, or it gets rapidly merged. 38 | - Do prioritize small PRs 39 | - All attendees & Apollo are happy to provide code-reviews & should be tagged in if more feedback is needed. 40 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # RestLink Docs 2 | 3 | The purpose of this directory is to have an in-repo copy of the `RestLink` documentation. It however truly lives in [`apollo-link`](https://www.apollographql.com/docs/link/links/rest.html) 4 | 5 | If you make changes in this directory, once the PR lands, please make sure it gets copied/merged into [apollographql/apollo-link](https://github.com/apollographql/apollo-link/blob/master/docs/source/links/rest.md) 6 | 7 | To help with this, we have these commands: 8 | 9 | ```shell 10 | # This will check to see if we're out of date 11 | $ yarn docs:check 12 | 13 | # Pushes documentation into a checkout of apollo-link for you to commit 14 | $ yarn docs:push 15 | 16 | # Pulls docs from apollo-link into your current checkout of apollo-link-rest 17 | $ yarn docs:pull 18 | ``` -------------------------------------------------------------------------------- /docs/rest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: apollo-link-rest 3 | description: Call your REST APIs inside your GraphQL queries. 4 | --- 5 | 6 | Calling REST APIs from a GraphQL client opens the benefits of GraphQL for more people, whether: 7 | 8 | * You are in a front-end developer team that wants to try GraphQL without asking for the backend team to implement a GraphQL server. 9 | * You have no access to change the backend because it's an existing set of APIs, potentially managed by a 3rd party. 10 | * You have an existing codebase, but you're looking to evaluate whether GraphQL can work for your needs. 11 | * You have a large codebase, and the GraphQL migration is happening on the backend, but you want to use GraphQL *now* without waiting! 12 | 13 | With `apollo-link-rest`, you can now call your endpoints inside your GraphQL queries and have all your data managed by [`ApolloClient`](https://www.apollographql.com/react/basics/setup/#ApolloClient). `apollo-link-rest` is suitable for just dipping your toes in the water, or doing a full-steam ahead integration, and then later on migrating to a backend-driven GraphQL experience. `apollo-link-rest` combines well with other links such as [`apollo-link-context`][], [`apollo-link-state`](/links/state/), and others! _For complex back-ends, you may want to consider using [`apollo-server`](https://www.apollographql.com/docs/apollo-server/) which you can try out at [launchpad.graphql.com](https://launchpad.graphql.com/)_ 14 | 15 | You can start using ApolloClient in your app today, let's see how! 16 | 17 | ## Quick start 18 | 19 | To get started, you need first to install @apollo/client: 20 | 21 | ```bash 22 | npm install --save @apollo/client 23 | ``` 24 | 25 | Then it is time to install our link and its `peerDependencies`: 26 | 27 | ```bash 28 | npm install --save apollo-link-rest graphql qs 29 | ``` 30 | 31 | After this, you are ready to setup your apollo client: 32 | 33 | ```js 34 | import { ApolloClient, InMemoryCache } from '@apollo/client'; 35 | import { RestLink } from 'apollo-link-rest'; 36 | 37 | // setup your `RestLink` with your endpoint 38 | const restLink = new RestLink({ uri: "https://swapi.co/api/" }); 39 | 40 | // setup your client 41 | const client = new ApolloClient({ 42 | link: restLink, 43 | cache: new InMemoryCache(), 44 | }); 45 | ``` 46 | 47 | From the [Apollo Client "Get started" docs](https://www.apollographql.com/docs/react/get-started#step-3-initialize-apolloclient): 48 | 49 | > `cache` is an instance of `InMemoryCache`, which Apollo Client uses to cache query results after fetching them. 50 | 51 | Now it is time to write our first query, for this you need to install the `graphql-tag` package: 52 | 53 | ```bash 54 | npm install --save graphql-tag 55 | ``` 56 | 57 | Defining a query is straightforward: 58 | 59 | ```js 60 | const query = gql` 61 | query luke { 62 | person @rest(type: "Person", path: "people/1/") { 63 | name 64 | } 65 | } 66 | `; 67 | ``` 68 | 69 | You can then fetch your data: 70 | 71 | ```js 72 | // Invoke the query and log the person's name 73 | client.query({ query }).then(response => { 74 | console.log(response.data.name); 75 | }); 76 | ``` 77 | 78 | ## Options 79 | 80 | Construction of `RestLink` takes an options object to customize the behavior of the link. The options you can pass are outlined below: 81 | 82 | * `uri: string`: the URI key is a string endpoint/domain for your requests to hit (_optional_ when `endpoints` provides a default) 83 | * `endpoints: /map-of-endpoints/`: _optional_ map of endpoints -- If you use this, you need to provide `endpoint` to the `@rest(...)` directives. 84 | * `customFetch?`: _optional_ a custom `fetch` to handle `REST` calls 85 | * `headers?: Headers`: _optional_ an object representing values to be sent as headers with all requests. [Documented here](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers) 86 | * `credentials?`: _optional_ a string representing the credentials policy the fetch call should operate with. [Documented here](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) 87 | * `fieldNameNormalizer?: /function/`: _optional_ function that takes the response field name and converts it into a GraphQL compliant name. -- This is useful if your `REST` API returns fields that aren't representable as GraphQL, or if you want to convert between `snake_case` field names in JSON to `camelCase` keyed fields. 88 | * `fieldNameDenormalizer?: /function/`: _optional_ function that takes a GraphQL-compliant field name and converts it back into an endpoint-specific name. 89 | * `typePatcher: /map-of-functions/`: _optional_ Structure to allow you to specify the `__typename` when you have nested objects in your REST response! 90 | * `defaultSerializer /function/`: _optional_ function that will be used by the `RestLink` as the default serializer when no `bodySerializer` is defined for a `@rest` call. The function will also be passed the current `Header` set, which can be updated before the request is sent to `fetch`. Default method uses `JSON.stringify` and sets the `Content-Type` to `application/json`. 91 | * `bodySerializers: /map-of-functions/`: _optional_ Structure to allow the definition of alternative serializers, which can then be specified by their key. 92 | * `responseTransformer?: /function/`: _optional_ Apollo expects a record response to return a root object, and a collection of records response to return an array of objects. Use this function to structure the response into the format Apollo expects if your response data is structured differently. 93 | 94 | ### Multiple endpoints 95 | 96 | If you want to be able to use multiple endpoints, you should create your link like so: 97 | 98 | ```js 99 | const link = new RestLink({ endpoints: { v1: 'api.com/v1', v2: 'api.com/v2' } }); 100 | ``` 101 | 102 | Then you need to specify in the rest directive the endpoint you want to use: 103 | 104 | ```js 105 | const postTitleQuery1 = gql` 106 | query postTitle { 107 | post @rest(type: "Post", path: "/post", endpoint: "v1") { 108 | id 109 | title 110 | } 111 | } 112 | `; 113 | const postTitleQuery2 = gql` 114 | query postTitle { 115 | post @rest(type: "[Tag]", path: "/tags", endpoint: "v2") { 116 | id 117 | tags 118 | } 119 | } 120 | `; 121 | ``` 122 | 123 | If you have a default endpoint, you can create your link like so: 124 | 125 | ```js 126 | const link = new RestLink({ 127 | endpoints: { github: 'github.com' }, 128 | uri: 'api.com', 129 | }); 130 | ``` 131 | 132 | Then if you do not specify an endpoint in your query the default endpoint (the one you specify in the `uri` option.) will be used. 133 | 134 | ### Typename patching 135 | 136 | When sending such a query: 137 | 138 | ```graphql 139 | query MyQuery { 140 | planets @rest(type: "PlanetPayload", path: "planets/") { 141 | count 142 | next 143 | results { 144 | name 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | The outer response object (`data.planets`) gets its `__typename: "PlanetPayload"` from the [`@rest(...)` directive's `type` parameter](#rest-directive). You, however, need to have a way to set the typename of `PlanetPayload.results`. 151 | 152 | One way you can do this is by providing a `typePatcher`: 153 | 154 | ```typescript 155 | const restLink = new RestLink({ 156 | uri: '/api', 157 | typePatcher: { 158 | PlanetPayload: ( 159 | data: any, 160 | outerType: string, 161 | patchDeeper: RestLink.FunctionalTypePatcher, 162 | context: RestLink.TypePatcherContext 163 | ): any => { 164 | if (data.results != null) { 165 | data.results = data.results.map( planet => ({ __typename: "Planet", ...planet })); 166 | } 167 | return data; 168 | }, 169 | /* … other nested type patchers … */ 170 | }, 171 | }) 172 | ``` 173 | 174 | If you have a very lightweight REST integration, you can use the `@type(name: ...)` directive. 175 | 176 | ```graphql 177 | query MyQuery { 178 | planets @rest(type: "PlanetPayload", path: "planets/") { 179 | count 180 | next 181 | results @type(name: "Planet") { 182 | name 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | This is appropriate if you have a small list of nested objects. The cost of this strategy is every query that deals with these objects needs to also include `@type(name: ...)` and this could be verbose and error prone. 189 | 190 | You can also use both of these approaches in tandem: 191 | 192 | ```graphql 193 | query MyQuery { 194 | planets @rest(type: "PlanetPayload", path: "planets/") { 195 | count 196 | next 197 | results @type(name: "Results") { 198 | name 199 | } 200 | typePatchedResults { 201 | name 202 | } 203 | } 204 | } 205 | ``` 206 | 207 | ```typescript 208 | const restLink = new RestLink({ 209 | uri: '/api', 210 | typePatcher: { 211 | PlanetPayload: ( 212 | data: any, 213 | outerType: string, 214 | patchDeeper: RestLink.FunctionalTypePatcher, 215 | context: RestLink.TypePatcherContext 216 | ): any => { 217 | if (data.typePatchedResults != null) { 218 | data.typePatchedResults = data.typePatchedResults.map( planet => { __typename: "Planet", ...planet }); 219 | } 220 | return data; 221 | }, 222 | /* … other nested type patchers … */ 223 | }, 224 | }) 225 | ``` 226 | 227 | If you want to take advantage of Apollo's built-in client caching, you can provide a unique id field for your types if one is not present by default in the response from the REST API. This assumes you are using a default `id` field as your unique identifier key. If you have changed this using `dataIdFromObject`, you will need to use a different identifier in the code. [See More](https://www.apollographql.com/docs/react/v2.5/advanced/caching/#configuration) 228 | 229 | ```typescript 230 | const restLink = new RestLink({ 231 | uri: '/api', 232 | typePatcher: { 233 | UserPayload: ( 234 | data: any, 235 | outerType: string, 236 | patchDeeper: RestLink.FunctionalTypePatcher, 237 | context: RestLink.TypePatcherContext 238 | ): any => { 239 | if (!data.id) { 240 | const { directives } = context.resolverParams.info; 241 | const { path, endpoint } = directives.rest as RestLink.DirectiveOptions; 242 | // path = 23483/summary 243 | // endpoint = user/ 244 | // So we are requesting /user/23483/summary 245 | data.id = path.substr(0, path.indexOf('/')); 246 | } 247 | return data; 248 | }, 249 | /* … other nested type patchers … */ 250 | }, 251 | }) 252 | ``` 253 | 254 | #### Warning 255 | 256 | However, you should know that at the moment the `typePatcher` is not able to act on nested objects within annotated `@type` objects. For instance, `failingResults` will not be patched if you define it on the `typePatcher`. 257 | 258 | ```graphql 259 | query MyQuery { 260 | planets @rest(type: "PlanetPayload", path: "planets/") { 261 | count 262 | next 263 | results @type(name: "Planet") { 264 | name 265 | failingResults { 266 | name 267 | } 268 | } 269 | typePatchedResults { 270 | name 271 | } 272 | } 273 | } 274 | ``` 275 | 276 | To make this work you should try to pick one strategy, and stick with it -- either all `typePatcher` or all `@type` directives. 277 | 278 | This is tracked in [Issue #112](https://github.com/apollographql/apollo-link-rest/issues/112) 279 | 280 | ### Response transforming 281 | 282 | By default, Apollo expects an object at the root for record requests, and an array of objects at the root for collection requests. For example, if fetching a user by ID (`/users/1`), the following response is expected. 283 | 284 | ```json 285 | { 286 | "id": 1, 287 | "name": "Apollo" 288 | } 289 | ``` 290 | 291 | And when fetching for a list of users (`/users`), the following response is expected. 292 | 293 | ```json 294 | [ 295 | { 296 | "id": 1, 297 | "name": "Apollo" 298 | }, 299 | { 300 | "id": 2, 301 | "name": "Starman" 302 | } 303 | ] 304 | ``` 305 | 306 | If the structure of your API responses differs than what Apollo expects, you can define a `responseTransformer` in the client. This function receives the response object as the 1st argument, and the current `typeName` as the 2nd argument. It should return a `Promise` as it will be responsible for reading the response stream by calling one of `json()`, `text()` etc. 307 | 308 | For instance if the record is not at the root level: 309 | 310 | ```json 311 | { 312 | "meta": {}, 313 | "data": [ 314 | { 315 | "id": 1, 316 | "name": "Apollo" 317 | }, 318 | { 319 | "id": 2, 320 | "name": "Starman" 321 | } 322 | ] 323 | } 324 | ``` 325 | 326 | The following transformer could be used to support it: 327 | 328 | 329 | ```js 330 | const link = new RestLink({ 331 | uri: '/api', 332 | responseTransformer: async response => response.json().then(({data}) => data), 333 | }); 334 | ``` 335 | 336 | Plaintext, or XML, or otherwise-encoded responses can be handled by manually parsing and converting them to JSON (using the previously described format that Apollo expects): 337 | 338 | ```js 339 | const link = new RestLink({ 340 | uri: '/xmlApi', 341 | responseTransformer: async response => response.text().then(text => parseXmlResponseToJson(text)), 342 | }); 343 | 344 | ``` 345 | 346 | ### Custom endpoint responses 347 | 348 | The client level `responseTransformer` applies for all responses, across all URIs and endpoints. If you need a custom `responseTransformer` per endpoint, you can define an object of options for that specific endpoint. 349 | 350 | ```js 351 | const link = new RestLink({ 352 | endpoints: { 353 | v1: { 354 | uri: '/v1', 355 | responseTransformer: async response => response.data, 356 | }, 357 | v2: { 358 | uri: '/v2', 359 | responseTransformer: async (response, typeName) => response[typeName], 360 | }, 361 | }, 362 | }); 363 | ``` 364 | 365 | > When using the object form, the `uri` field is required. 366 | 367 | ### Custom Fetch 368 | 369 | By default, Apollo uses the browsers `fetch` method to handle `REST` requests to your domain/endpoint. The `customFetch` option allows you to specify _your own_ request handler by defining a function that returns a `Promise` with a fetch-response-like object: 370 | ```js 371 | const link = new RestLink({ 372 | endpoints: "/api", 373 | customFetch: (uri, options) => new Promise((resolve, reject) => { 374 | // Your own (asynchronous) request handler 375 | resolve(responseObject) 376 | }), 377 | }); 378 | ``` 379 | 380 | To resolve your GraphQL queries quickly, Apollo will issue requests to relevant endpoints as soon as possible. This is generally ok, but can lead to large numbers of `REST` requests to be fired at once; especially for deeply nested queries [(see `@export` directive)](#export-directive). 381 | 382 | > Some endpoints (like public APIs) might enforce _rate limits_, leading to failed responses and unresolved queries in such cases. 383 | 384 | By example, `customFetch` is a good place to manage your apps fetch operations. The following implementation makes sure to only issue 2 requests at a time (concurrency) while waiting at least 500ms until the next batch of requests is fired. 385 | 386 | ```js 387 | import pThrottle from "p-throttle"; 388 | 389 | const link = new RestLink({ 390 | endpoints: "/api", 391 | customFetch: pThrottle((uri, config) => { 392 | return fetch(uri, config); 393 | }, 394 | 2, // Max. concurrent Requests 395 | 500 // Min. delay between calls 396 | ), 397 | }); 398 | ``` 399 | > Since Apollo issues `Promise` based requests, we can resolve them as we see fit. This example uses [`pThrottle`](https://github.com/sindresorhus/p-throttle); part of the popular [promise-fun](https://github.com/sindresorhus/promise-fun) collection. 400 | 401 | ### Complete options 402 | 403 | Here is one way you might customize `RestLink`: 404 | 405 | ```js 406 | import fetch from 'node-fetch'; 407 | import * as camelCase from 'camelcase'; 408 | import * as snake_case from 'snake-case'; 409 | 410 | const link = new RestLink({ 411 | endpoints: { github: 'github.com' }, 412 | uri: 'api.com', 413 | customFetch: fetch, 414 | headers: { 415 | "Content-Type": "application/json" 416 | }, 417 | credentials: "same-origin", 418 | fieldNameNormalizer: (key: string) => camelCase(key), 419 | fieldNameDenormalizer: (key: string) => snake_case(key), 420 | typePatcher: { 421 | Post: ()=> { 422 | bodySnippet... 423 | } 424 | }, 425 | defaultSerializer: (data: any, headers: Headers) => { 426 | const formData = new FormData(); 427 | for (let key in body) { 428 | formData.append(key, body[key]); 429 | } 430 | headers.set("Content-Type", "x-www-form-encoded") 431 | return {body: formData, headers}; 432 | } 433 | }); 434 | ``` 435 | 436 | ## Link Context 437 | 438 | `RestLink` has an [interface `LinkChainContext`](https://github.com/apollographql/apollo-link-rest/blob/1824da47d5db77a2259f770d9c9dd60054c4bb1c/src/restLink.ts#L557-L570) which it uses as the structure of things that it will look for in the `context`, as it decides how to fulfill a specific `RestLink` request. (Please see the [`apollo-link-context`][] page for a discussion of why you might want this). 439 | 440 | * `credentials?: RequestCredentials`: overrides the `RestLink`-level setting for `credentials`. [Values documented here](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers) 441 | * `headers?: Headers`: Additional headers provided in this `context-link` [Values documented here](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers) 442 | * `headersToOverride?: string[]` If you provide this array, we will merge the headers you provide in this link, by replacing any matching headers that exist in the root `RestLink` configuration. Alternatively you can use `headersMergePolicy` for more fine-grained customization of the merging behavior. 443 | * `headersMergePolicy?: RestLink.HeadersMergePolicy`: This is a function that decide how the headers returned in this `contextLink` are merged with headers defined at the `RestLink`-level. If you don't provide this, the headers will be simply appended. To use this option, you can provide your own function that decides how to process the headers. [Code references](https://github.com/apollographql/apollo-link-rest/blob/8e57cabb5344209d9cfa391c1614fe8880efa5d9/src/restLink.ts#L462-L510) 444 | * `restResponses?: Response[]`: This will be populated after the operation has completed with the [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response) of every REST url fetched during the operation. This can be useful if you need to access the response headers to grab an authorization token for example. 445 | 446 | ### Example 447 | 448 | `RestLink` uses the `headers` field on the [`apollo-link-context`][] so you can compose other links that provide additional & dynamic headers to a given query. 449 | 450 | [`apollo-link-context`]: /links/context/ 451 | 452 | Here is one way to add request `headers` to the context and retrieve the response headers of the operation: 453 | 454 | ```js 455 | const authRestLink = new ApolloLink((operation, forward) => { 456 | operation.setContext(({headers}) => { 457 | const token = localStorage.getItem("token"); 458 | return { 459 | headers: { 460 | ...headers, 461 | Accept: "application/json", 462 | Authorization: token 463 | } 464 | }; 465 | }); 466 | return forward(operation).map(result => { 467 | const { restResponses } = operation.getContext(); 468 | const authTokenResponse = restResponses.find(res => res.headers.has("Authorization")); 469 | // You might also filter on res.url to find the response of a specific API call 470 | if (authTokenResponse) { 471 | localStorage.setItem("token", authTokenResponse.headers.get("Authorization")); 472 | } 473 | return result; 474 | }); 475 | }); 476 | 477 | const restLink = new RestLink({ uri: "uri" }); 478 | 479 | const client = new ApolloClient({ 480 | link: ApolloLink.from([authRestLink, restLink]), 481 | cache: new InMemoryCache(), 482 | }); 483 | ``` 484 | 485 | ## Link order 486 | 487 | If you are using multiple link types, `restLink` should go before `httpLink`, as `httpLink` will swallow any calls that should be routed through `apollo-link-rest`! 488 | 489 | For example: 490 | 491 | ```js 492 | const httpLink = createHttpLink({ uri: "server.com/graphql" }); 493 | const restLink = new RestLink({ uri: "api.server.com" }); 494 | 495 | const client = new ApolloClient({ 496 | link: ApolloLink.from([authLink, restLink, errorLink, retryLink, httpLink]), 497 | // Note: httpLink is terminating so must be last, while retry & error wrap the links to their right 498 | // state & context links should happen before (to the left of) restLink. 499 | cache: new InMemoryCache() 500 | }); 501 | ``` 502 | 503 | _Note: you should also consider this if you're using [`apollo-link-context`](#link-context) to set `Headers`, you need that link to be before `restLink` as well._ 504 | 505 | ## @rest directive 506 | 507 | This is where you setup the endpoint you want to fetch. 508 | The rest directive could be used at any depth in a query, but once it is used, nothing nested in it can be GraphQL data, it has to be from the `RestLink` or other resource (like the [`@client` directive](/links/state/)) 509 | 510 | ### Arguments 511 | 512 | An `@rest(…)` directive takes two required and several optional arguments: 513 | 514 | * `type: string`: The GraphQL type this will return 515 | * `path: string`: uri-path to the REST API. This could be a path or a full url. If a path, the endpoint given on link creation or from the context is concatenated with it to produce a full `URI`. See also: `pathBuilder` 516 | * _optional_ `method?: "GET" | "PUT" | "POST" | "DELETE"`: the HTTP method to send the request via (i.e GET, PUT, POST) 517 | * _optional_ `endpoint?: string` key to use when looking up the endpoint in the (optional) `endpoints` table if provided to RestLink at creation time. 518 | * _optional_ `pathBuilder?: /function/`: If provided, this function gets to control what path is produced for this request. 519 | * _optional_ `bodyKey?: string = "input"`: This is the name of the `variable` to use when looking to build a REST request-body for a `PUT` or `POST` request. It defaults to `input` if not supplied. 520 | * _optional_ `bodyBuilder?: /function/`: If provided, this is the name a `function` that you provided to `variables`, that is called when a request-body needs to be built. This lets you combine arguments or encode the body in some format other than JSON. 521 | * _optional_ `bodySerializer?: /string | function/`: string key to look up a function in `bodySerializers` or a custom serialization function for the body/headers of this request before it is passed to the fetch call. Defaults to `JSON.stringify` and setting `Content-Type: application-json`. 522 | 523 | ### Variables 524 | 525 | You can use query `variables` inside nested queries, or in the the path argument of your directive: 526 | 527 | ```graphql 528 | query postTitle { 529 | post(id: "1") @rest(type: "Post", path: "/post/{args.id}") { 530 | id 531 | title 532 | } 533 | } 534 | ``` 535 | 536 | *Warning*: Variables in the main path will not automatically have `encodeURIComponent` called on them 537 | 538 | Additionally, you can also control the query-string: 539 | 540 | ```graphql 541 | query postTitle { 542 | postSearch(query: "some key words", page_size: 5) 543 | @rest(type: "Post", path: "/search?{args}&{context.language}") { 544 | id 545 | title 546 | } 547 | } 548 | ``` 549 | 550 | Things to note: 551 | 552 | 1. This will be converted into `/search?query=some%20key%20words&page_size=5&lang=en` 553 | 2. The `context.language / lang=en` is extracting an object from the Apollo Context, that was added via an `apollo-link-context` Link. 554 | 3. The query string arguments are assembled by npm:qs and have `encodeURIComponent` called on them. 555 | 556 | The available variable sources are: 557 | 558 | * `args` these are the things passed directly to this field parameters. In the above example `postSearch` had `query` and `page_size` in args. 559 | * `exportVariables` these are the things in the parent context that were tagged as `@export(as: ...)` 560 | * `context` these are the apollo-context, so you can have globals set up via `apollo-link-context` 561 | * `@rest` these include any other parameters you pass to the `@rest()` directive. This is probably more useful when working with `pathBuilder`, documented below. 562 | 563 | #### `pathBuilder` 564 | 565 | If the variable-replacement options described above aren't enough, you can provide a `pathBuilder` to your query. This will be called to dynamically construct the path. This is considered an advanced feature, and is documented in the source -- it also should be considered syntactically unstable, and we're looking for feedback! 566 | 567 | #### `bodyKey` / `bodyBuilder` 568 | 569 | When making a `POST` or `PUT` HTTP request, you often need to provide a request body. By [convention](https://graphql.org/graphql-js/mutations-and-input-types/), GraphQL recommends you name your input-types as `input`, so by default that's where we'll look to find a JSON object for your body. 570 | 571 | ##### `bodyKey` 572 | 573 | If you need/want to name it something different, you can pass `bodyKey`, and we'll look at that variable instead. 574 | 575 | In this example the publish API accepts a body in the variable `body` instead of input: 576 | 577 | ```graphql 578 | mutation publishPost( 579 | $someApiWithACustomBodyKey: PublishablePostInput! 580 | ) { 581 | publishedPost: publish(input: "Foo", body: $someApiWithACustomBodyKey) 582 | @rest( 583 | type: "Post" 584 | path: "/posts/{args.input}/new" 585 | method: "POST" 586 | bodyKey: "body" 587 | ) { 588 | id 589 | title 590 | } 591 | } 592 | ``` 593 | 594 | [Unit Test](https://github.com/apollographql/apollo-link-rest/blob/c9d81ae308e5f61b5ae992061de7abc6cb2f78e0/src/__tests__/restLink.ts#L1803-L1846) 595 | 596 | ##### `bodyBuilder` 597 | 598 | If you need to structure your data differently, or you need to custom encode your body (say as form-encoded), you can instead provide `bodyBuilder` 599 | 600 | ```graphql 601 | mutation encryptedPost( 602 | $input: PublishablePostInput! 603 | $encryptor: any 604 | ) { 605 | publishedPost: publish(input: $input) 606 | @rest( 607 | type: "Post" 608 | path: "/posts/new" 609 | method: "POST" 610 | bodyBuilder: $encryptor 611 | ) { 612 | id 613 | title 614 | } 615 | } 616 | ``` 617 | 618 | [Unit Test](https://github.com/apollographql/apollo-link-rest/blob/c9d81ae308e5f61b5ae992061de7abc6cb2f78e0/src/__tests__/restLink.ts#L1847-L1904) 619 | 620 | ##### `bodySerializer` 621 | 622 | If you need to serialize your data differently (say as form-encoded), you can provide a `bodySerializer` instead of relying on the default JSON serialization. 623 | `bodySerializer` can be either a function of the form `(data: any, headers: Headers) => {body: any, headers: Headers}` or a string key. When using the string key 624 | `RestLink` will instead use the corresponding serializer from the `bodySerializers` object that can optionally be passed in during initialization. 625 | 626 | ```graphql 627 | mutation encryptedForm( 628 | $input: PublishablePostInput!, 629 | $formSerializer: any 630 | ) { 631 | publishedPost: publish(input: $input) 632 | @rest( 633 | type: "Post", 634 | path: "/posts/new", 635 | method: "POST", 636 | bodySerializer: $formSerializer 637 | ) { 638 | id 639 | title 640 | } 641 | 642 | publishRSS(input: $input) 643 | @rest( 644 | type: "Post", 645 | path: "/feed", 646 | method: "POST", 647 | bodySerializer: "xml" 648 | ) 649 | } 650 | ``` 651 | 652 | Where `formSerializer` could be defined as 653 | 654 | ```typescript 655 | const formSerializer = (data: any, headers: Headers) => { 656 | const formData = new FormData(); 657 | for (let key in data) { 658 | if (data.hasOwnProperty(key)) { 659 | formData.append(key, data[key]); 660 | } 661 | } 662 | 663 | headers.set('Content-Type', 'application/x-www-form-urlencoded'); 664 | 665 | return {body: formData, headers}; 666 | } 667 | 668 | ``` 669 | 670 | And `"xml"` would have been defined on the `RestLink` directly 671 | 672 | ```typescript 673 | const restLink = new RestLink({ 674 | ...otherOptions, 675 | bodySerializers: { 676 | xml: xmlSerializer 677 | } 678 | }) 679 | ``` 680 | 681 | ## @export directive 682 | 683 | The export directive re-exposes a field for use in a later (nested) query. These are the same semantics that will be supported on the server, but when used in a `RestLink` you can use the exported variables for further calls (i.e. waterfall requests from nested fields) 684 | 685 | _Note: If you're constantly using @export you may prefer to take a look at [`apollo-server`](https://www.apollographql.com/docs/apollo-server/) which you can try out at [launchpad.graphql.com](https://launchpad.graphql.com/)_ 686 | 687 | ### Arguments 688 | 689 | * `as: string`: name to create this as a variable to be used down the selection set 690 | 691 | ### Example 692 | 693 | An example use-case would be getting a list of users, and hitting a different endpoint to fetch more data using the exported field in the REST query args. 694 | 695 | ```graphql 696 | const QUERY = gql` 697 | query RestData($email: String!) { 698 | users @rest(path: '/users/email?{args.email}', method: 'GET', type: 'User') { 699 | id @export(as: "id") 700 | firstName 701 | lastName 702 | friends @rest(path: '/friends/{exportVariables.id}', type: '[User]') { 703 | firstName 704 | lastName 705 | } 706 | } 707 | } 708 | `; 709 | ``` 710 | 711 | ## Mutations 712 | 713 | You can write also mutations with the apollo-link-rest, for example: 714 | 715 | ```graphql 716 | mutation deletePost($id: ID!) { 717 | deletePostResponse(id: $id) 718 | @rest(type: "Post", path: "/posts/{args.id}", method: "DELETE") { 719 | NoResponse 720 | } 721 | } 722 | ``` 723 | 724 | ## Troubleshooting 725 | 726 | As you start using `apollo-link-rest` you may run into some standard issues that we thought we could help you solve. 727 | 728 | * `Missing field __typename in ...` -- If you see this, it's possible you haven't provided `type:` to the [`@rest(...)`](#rest-directive)-directive. Alternately you need to set up a [`typePatcher`](#typename-patching) 729 | * `Headers is undefined` -- If you see something like this, you're running in a browser or other Javascript environment that does not yet support the full specification for the `Headers` API. 730 | 731 | ## Example apps 732 | 733 | To get you started, here are some example apps: 734 | 735 | * [Simple](https://github.com/apollographql/apollo-link-rest/tree/master/examples/simple): 736 | A very simple app with a single query that reflect the setup section. 737 | * [Advanced](https://github.com/apollographql/apollo-link-rest/tree/master/examples/advanced): 738 | A more complex app that demonstrate how to use an export directive. 739 | 740 | ## Contributing 741 | 742 | Please join us on github: [apollographql/apollo-link-rest](https://github.com/apollographql/apollo-link-rest/) and in the ApolloGraphQL Slack in the `#apollo-link-rest` chat room. 743 | 744 | If you have an example app that you'd like to be featured, please send us a PR! 😊 We'd love to hear how you're using `apollo-link-rest`. 745 | -------------------------------------------------------------------------------- /examples/advanced/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/advanced/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 2 | 3 | -------------------------------------------------------------------------------- /examples/advanced/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "3.7.1", 7 | "apollo-link-rest": "0.x", 8 | "graphql": "16.6.0", 9 | "graphql-tag": "2.12.6", 10 | "qs": "6.11.0", 11 | "react": "18.2.0", 12 | "react-dom": "18.2.0", 13 | "react-scripts": "5.0.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/advanced/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-link-rest/bd0e244b9826e8e9053296e87bbb081a4b4cd394/examples/advanced/public/favicon.ico -------------------------------------------------------------------------------- /examples/advanced/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/advanced/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #222; 7 | padding: 20px; 8 | color: white; 9 | } 10 | 11 | .App-title { 12 | font-size: 1.5em; 13 | } 14 | 15 | .App-intro { 16 | font-size: large; 17 | } 18 | 19 | @keyframes App-logo-spin { 20 | from { transform: rotate(0deg); } 21 | to { transform: rotate(360deg); } 22 | } 23 | -------------------------------------------------------------------------------- /examples/advanced/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 3 | import { RestLink } from 'apollo-link-rest'; 4 | import SearchShow from './SearchShow'; 5 | import './App.css'; 6 | 7 | const restLink = new RestLink({ 8 | uri: 'https://api.tvmaze.com/', 9 | }); 10 | 11 | const client = new ApolloClient({ 12 | link: restLink, 13 | cache: new InMemoryCache(), 14 | }); 15 | 16 | class App extends Component { 17 | render() { 18 | return ( 19 |
20 |
21 |

Welcome to Apollo Rest Link Example

22 |
23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | const ApolloApp = () => ( 30 | 31 | 32 | 33 | ); 34 | 35 | export default ApolloApp; 36 | -------------------------------------------------------------------------------- /examples/advanced/src/SearchShow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from '@apollo/client/react/hoc'; 3 | import gql from 'graphql-tag'; 4 | 5 | const Season = ({ summary, number, image }) => ( 6 |
7 |

{`Season ${number}`}

8 | {image && } 9 |
10 |
11 | ); 12 | 13 | const Query = gql` 14 | query($searchInput: String!) { 15 | show(search: $searchInput) 16 | @rest(type: "People", path: "singlesearch/shows?q=:search") { 17 | id @export(as: "showId") 18 | name 19 | seasons @rest(type: "Season", path: "shows/:showId/seasons") { 20 | number 21 | image 22 | summary 23 | } 24 | } 25 | } 26 | `; 27 | 28 | class ShowsResult extends Component { 29 | render() { 30 | const { 31 | data: { loading, error, show }, 32 | } = this.props; 33 | if (loading) { 34 | return

Loading...

; 35 | } 36 | if (error) { 37 | return

{error.message}

; 38 | } 39 | return ( 40 |
41 |

{show.name}

42 | {show.seasons.map(({ number, image, summary }) => ( 43 | 49 | ))} 50 |
51 | ); 52 | } 53 | } 54 | 55 | const ShowsResultQuery = graphql(Query, { 56 | options: ({ searchInput }) => { 57 | return { variables: { searchInput } }; 58 | }, 59 | })(ShowsResult); 60 | 61 | class SearchShow extends Component { 62 | constructor() { 63 | super(); 64 | this.state = { 65 | searchInput: '', 66 | }; 67 | } 68 | render() { 69 | return ( 70 |
71 | this.setState({ searchInput: e.target.value })} 75 | /> 76 | 80 | {this.state.searchInput !== '' && ( 81 | 82 | )} 83 |
84 | ); 85 | } 86 | } 87 | 88 | export default SearchShow; 89 | -------------------------------------------------------------------------------- /examples/advanced/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/advanced/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "3.7.1", 7 | "apollo-link-rest": "0.x", 8 | "graphql": "16.6.0", 9 | "graphql-tag": "2.12.6", 10 | "qs": "6.11.0", 11 | "react": "18.2.0", 12 | "react-dom": "18.2.0", 13 | "react-scripts": "5.0.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/simple/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-link-rest/bd0e244b9826e8e9053296e87bbb081a4b4cd394/examples/simple/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Testing Apollo Rest Link 9 | 10 | 11 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/simple/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #222; 7 | padding: 20px; 8 | color: white; 9 | } 10 | 11 | .App-title { 12 | font-size: 1.5em; 13 | } 14 | 15 | .App-intro { 16 | font-size: large; 17 | } 18 | 19 | @keyframes App-logo-spin { 20 | from { transform: rotate(0deg); } 21 | to { transform: rotate(360deg); } 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 3 | import { RestLink } from 'apollo-link-rest'; 4 | import Person from './Person'; 5 | import './App.css'; 6 | 7 | const restLink = new RestLink({ 8 | uri: 'https://swapi.dev/api/', 9 | }); 10 | 11 | const client = new ApolloClient({ 12 | link: restLink, 13 | cache: new InMemoryCache(), 14 | }); 15 | 16 | class App extends Component { 17 | render() { 18 | return ( 19 |
20 |
21 |

Welcome to Apollo Rest Link Example

22 |
23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | const ApolloApp = () => ( 30 | 31 | 32 | 33 | ); 34 | 35 | export default ApolloApp; 36 | -------------------------------------------------------------------------------- /examples/simple/src/Person.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from '@apollo/client/react/hoc'; 3 | import gql from 'graphql-tag'; 4 | 5 | const Query = gql` 6 | query luke { 7 | person @rest(type: "Person", path: "people/1/") { 8 | name 9 | } 10 | } 11 | `; 12 | 13 | class Person extends Component { 14 | render() { 15 | const { loading, error, person } = this.props; 16 | if (loading) { 17 | return

Loading...

; 18 | } 19 | if (error) { 20 | return

{error.message}

; 21 | } 22 | return

{person.name}

; 23 | } 24 | } 25 | export default graphql(Query, { 26 | props: ({ data }) => { 27 | if (data.loading) { 28 | return { 29 | loading: data.loading, 30 | }; 31 | } 32 | 33 | if (data.error) { 34 | return { 35 | error: data.error, 36 | }; 37 | } 38 | return { 39 | person: data.person, 40 | loading: false, 41 | }; 42 | }, 43 | })(Person); 44 | -------------------------------------------------------------------------------- /examples/simple/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 2 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "src/index.tsx", 6 | "dependencies": { 7 | "@apollo/client": "3.7.1", 8 | "apollo-link-rest": "0.x", 9 | "graphql": "16.6.0", 10 | "graphql-tag": "2.12.6", 11 | "qs": "6.11.0", 12 | "react": "18.2.0", 13 | "react-dom": "18.2.0", 14 | "react-scripts": "5.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "29.2.0", 24 | "@types/node": "18.11.4", 25 | "@types/react": "18.0.22", 26 | "@types/react-dom": "18.0.7", 27 | "typescript": "4.8.4" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/typescript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-link-rest/bd0e244b9826e8e9053296e87bbb081a4b4cd394/examples/typescript/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Apollo REST Link & TypeScript 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/typescript/src/Repo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { graphql, ChildProps } from '@apollo/client/react/hoc'; 3 | import gql from 'graphql-tag'; 4 | 5 | // The Result type we expect back. 6 | // See https://developer.github.com/v3/repos/#get 7 | interface Result { 8 | repo: { 9 | id: number; 10 | name: string; 11 | description: string; 12 | html_url: string; 13 | }; 14 | } 15 | 16 | // The props we expect to be passed directly to this component. 17 | interface OwnProps { 18 | name: string; 19 | } 20 | 21 | // Define the Props for the Repo component using React Apollo's 22 | // ChildProps generic inteface with the expected Result. 23 | type Props = ChildProps; 24 | 25 | // Standard React Component, using the injected data prop. 26 | class RepoBase extends React.Component { 27 | public render() { 28 | const { data } = this.props; 29 | 30 | if (data && data.repo) { 31 | return ( 32 |
33 |

34 | {data.repo.name} 35 |

36 |

{data.repo.description}

37 |
38 | ); 39 | } else if (data && data.loading) { 40 | return
Loading...
; 41 | } else { 42 | return null; 43 | } 44 | } 45 | } 46 | 47 | // Setup a basic query to retrieve data for that repository given a name 48 | const query = gql` 49 | query Repo($name: String!) { 50 | repo(name: $name) @rest(type: "Repo", path: "/repos/apollographql/:name") { 51 | id 52 | name 53 | description 54 | html_url 55 | } 56 | } 57 | `; 58 | 59 | // Connect the component using React Apollo's higher order component 60 | // and inject the data into the component. The Result type is what 61 | // we expect the shape of the response to be and OwnProps is what we 62 | // expect to be passed to this component. 63 | const Repo = graphql(query, { 64 | options: ({ name }) => ({ variables: { name } }), 65 | })(RepoBase); 66 | 67 | export { Repo }; 68 | -------------------------------------------------------------------------------- /examples/typescript/src/RepoSearch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Repo } from './Repo'; 4 | 5 | interface State { 6 | repo: string; 7 | } 8 | 9 | // A basic dropdown component to select between several options. 10 | class RepoSearch extends React.Component<{}, State> { 11 | constructor(props: {}) { 12 | super(props); 13 | 14 | this.state = { 15 | repo: 'apollo-link-rest', 16 | }; 17 | } 18 | 19 | public render() { 20 | return ( 21 |
22 | 23 | 33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | export { RepoSearch }; 41 | -------------------------------------------------------------------------------- /examples/typescript/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | label { 8 | display: block; 9 | margin-bottom: 4px; 10 | } 11 | 12 | select { 13 | font-size: 18px; 14 | } 15 | 16 | a { 17 | color: #2ca599; 18 | } 19 | 20 | h3 { 21 | margin: 32px 0 8px; 22 | padding: 0; 23 | } 24 | 25 | p { 26 | margin: 0; 27 | } 28 | 29 | .container { 30 | max-width: 360px; 31 | margin: 16px auto; 32 | } 33 | -------------------------------------------------------------------------------- /examples/typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { RestLink } from 'apollo-link-rest'; 4 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 5 | 6 | import { RepoSearch } from './RepoSearch'; 7 | 8 | import './index.css'; 9 | 10 | // Create a RestLink for the Github API 11 | const link = new RestLink({ uri: 'https://api.github.com' }); 12 | 13 | // Configure the ApolloClient with the recommended cache and our RestLink 14 | const client = new ApolloClient({ 15 | cache: new InMemoryCache(), 16 | link, 17 | }); 18 | 19 | const container = document.getElementById('root')!; 20 | const root = createRoot(container); 21 | 22 | root.render( 23 | 24 | 25 | , 26 | ); 27 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom", "esnext.asynciterable"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "strictNullChecks": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "build", 23 | "scripts", 24 | "acceptance-tests", 25 | "webpack", 26 | "jest", 27 | "src/setupTests.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | globals: { 4 | 'ts-jest': { 5 | babelConfig: false, 6 | mapCoverage: true, 7 | compilerOptions: { 8 | allowJs: true, // Necessary for jest.js 9 | }, 10 | diagnostics: { 11 | ignoreCodes: [ 12 | 151001 // Suppress esModuleInterop suggestion that breaks __tests__/restLink.ts 13 | ] 14 | } 15 | }, 16 | }, 17 | transform: { 18 | '.(ts|tsx)': 'ts-jest', 19 | }, 20 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', 21 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 22 | setupFiles: ['./scripts/jest.js'], 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-link-rest", 3 | "version": "0.9.0", 4 | "description": "Query existing REST services with GraphQL", 5 | "license": "MIT", 6 | "main": "./lib/bundle.umd.js", 7 | "module": "./lib/index.js", 8 | "jsnext:main": "./lib/index.js", 9 | "typings": "./lib/index.d.ts", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/apollographql/apollo-link-rest.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/apollographql/apollo-link-rest/issues" 16 | }, 17 | "homepage": "https://github.com/apollographql/apollo-link-rest#readme", 18 | "scripts": { 19 | "build:browser": "browserify ./lib/bundle.umd.js -o=./lib/bundle.js --i @apollo/client/core --i @apollo/client/utilities --i graphql --i react && npm run minify:browser", 20 | "build": "tsc -p .", 21 | "bundle": "rollup -c", 22 | "clean": "rimraf lib/* coverage/* npm/*", 23 | "clean:modules": "rm -rf node_modules/", 24 | "coverage:upload": "codecov", 25 | "danger": "danger run --verbose", 26 | "deploy": "./scripts/deploy.sh", 27 | "docs:check": "./scripts/docs_check.sh", 28 | "docs:pull": "./scripts/docs_pull.sh", 29 | "docs:push": "./scripts/docs_push.sh", 30 | "filesize": "npm run build && npm run build:browser && bundlesize", 31 | "lint": "prettier --write 'src/**/*.{j,t}s*'", 32 | "lint-staged": "lint-staged", 33 | "minify:browser": "uglifyjs -c -m -o ./lib/bundle.min.js -- ./lib/bundle.js", 34 | "postbuild": "npm run bundle", 35 | "prebuild": "npm run clean", 36 | "prepublishOnly": "npm run clean && npm run build", 37 | "prettier": "prettier --config .prettierrc", 38 | "prettier:diff": "prettier --config .prettierrc --list-different \"src/**/*.{ts,tsx,js,jsx}\" || true", 39 | "prettier:diff-with-error": "prettier --config .prettierrc --list-different \"src/**/*.{ts,tsx,js,jsx}\"", 40 | "prettier:all": "yarn prettier --write \"./src/**/*.{ts,tsx,js,jsx}\" ", 41 | "test": "jest", 42 | "coverage": "npm run lint && jest --coverage", 43 | "watch": "tsc -w -p .", 44 | "check-types": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.tests.json" 45 | }, 46 | "peerDependencies": { 47 | "@apollo/client": ">=3", 48 | "graphql": ">=0.11", 49 | "qs": ">=6" 50 | }, 51 | "devDependencies": { 52 | "@apollo/client": "^3.0.0-beta.29", 53 | "@apollo/link-error": "^2.0.0-beta.3", 54 | "@babel/core": "7.x", 55 | "@types/graphql": "14.x", 56 | "@types/jest": "23.x", 57 | "@types/node": "10.x", 58 | "@types/qs": "6.5.x", 59 | "browserify": "16.2.x", 60 | "bundlesize": "0.17.x", 61 | "camelcase": "5.0.x", 62 | "codecov": "3.x", 63 | "danger": "6.x", 64 | "fetch-mock": "7.x", 65 | "graphql": "14.x", 66 | "isomorphic-fetch": "2.2.x", 67 | "jest": "23.x", 68 | "jest-fetch-mock": "2.x", 69 | "lerna": "3.6.x", 70 | "lint-staged": "8.1.x", 71 | "lodash": "4.17.x", 72 | "pre-commit": "1.2.x", 73 | "prettier": "1.15.x", 74 | "qs": "6.6.x", 75 | "rimraf": "2.6.x", 76 | "rollup": "0.67.x", 77 | "rollup-plugin-local-resolve": "1.0.x", 78 | "rollup-plugin-sourcemaps": "0.4.x", 79 | "snake-case": "2.1.x", 80 | "ts-jest": "23.10.x", 81 | "typescript": "3.x", 82 | "uglify-js": "3.4.x" 83 | }, 84 | "resolutions": { 85 | "babel-core": "7.0.0-bridge.0", 86 | "babel-jest": "23.6.0" 87 | }, 88 | "bundlesize": [ 89 | { 90 | "name": "apollo-link-rest", 91 | "path": "./lib/bundle.min.js", 92 | "maxSize": "9.5 kb" 93 | } 94 | ], 95 | "lint-staged": { 96 | "*.ts*": [ 97 | "prettier --write", 98 | "git add" 99 | ], 100 | "*.js*": [ 101 | "prettier --write", 102 | "git add" 103 | ], 104 | "*.json*": [ 105 | "prettier --write", 106 | "git add" 107 | ] 108 | }, 109 | "pre-commit": "lint-staged" 110 | } 111 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "rangeStrategy": "pin", 3 | "semanticCommits": "enabled", 4 | "packageRules": [ 5 | { 6 | "rangeStrategy": "replace", 7 | "matchDepTypes": ["dependencies"] 8 | } 9 | ], 10 | "timezone": "America/Los_Angeles", 11 | "schedule": ["after 10pm every weekday", "before 5am every weekday"], 12 | "rebaseWhen": "behind-base-branch", 13 | "prCreation": "not-pending", 14 | "minor": { 15 | "automerge": true 16 | }, 17 | "major": { 18 | "automerge": false 19 | }, 20 | "labels": ["tooling", "dependencies"], 21 | "assignees": ["@fbartho"], 22 | "reviewers": ["@fbartho"] 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-local-resolve'; 2 | import sourcemaps from 'rollup-plugin-sourcemaps'; 3 | 4 | const globals = { 5 | '@apollo/client/core': 'apolloClient.core', 6 | '@apollo/client/utilities': 'apolloClient.utilities', 7 | '@apollo/link-error': 'apolloLink.error', 8 | }; 9 | 10 | export default { 11 | input: 'lib/index.js', 12 | output: { 13 | file: 'lib/bundle.umd.js', 14 | format: 'umd', 15 | exports: 'named', 16 | name: 'apollo-link-rest', 17 | 18 | globals, 19 | sourcemap: true, 20 | }, 21 | external: Object.keys(globals), 22 | onwarn, 23 | plugins: [resolve(), sourcemaps()], 24 | }; 25 | 26 | function onwarn(message) { 27 | const suppressed = ['UNRESOLVED_IMPORT', 'THIS_IS_UNDEFINED']; 28 | 29 | if (!suppressed.find(code => message.code === code)) { 30 | return console.warn(message.message); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | scalar RestFunction 2 | union RestFunctionOrString = String | RestFunction 3 | 4 | """ 5 | Set up the endpoint you want to fetch over REST. The rest directive could be used 6 | at any depth in a query, but once it is used, nothing nested in it can be GraphQL 7 | data, it has to be from the RestLink or other resource (like the @client directive) 8 | """ 9 | directive @rest( 10 | """ 11 | The GraphQL type this will return 12 | """ 13 | type: String! 14 | """ 15 | URI-path to the REST API. This could be a path or a full URL. If a path, the 16 | endpoint given on link creation or from the context is concatenated with it to 17 | produce a full `URI`. See also: `pathBuilder` 18 | """ 19 | path: String! 20 | """ 21 | The HTTP method to send the request via (i.e GET, PUT, POST) 22 | """ 23 | method: String 24 | """ 25 | Key to use when looking up the endpoint in the (optional) `endpoints` table if 26 | provided to `RestLink` at creation time. 27 | """ 28 | endpoint: String 29 | """ 30 | If provided, this function gets to control what path is produced for this request. 31 | """ 32 | pathBuilder: RestFunction 33 | """ 34 | This is the name of the variable to use when looking to build a REST request-body 35 | for a `PUT` or `POST` request. It defaults to `input` if not supplied. 36 | """ 37 | bodyKey: String = "input" 38 | """ 39 | If provided, this is the name a `function` that you provided to `variables`, that is 40 | called when a request-body needs to be built. This lets you combine arguments or 41 | encode the body in some format other than JSON. 42 | """ 43 | bodyBuilder: RestFunction 44 | """ 45 | String key to look up a function in `bodySerializers` or a custom serialization 46 | function for the body/headers of this request before it is passed to the fetch call. 47 | Defaults to `JSON.stringify` and setting `Content-Type: application-json`. 48 | """ 49 | bodySerializer: RestFunctionOrString 50 | ) on FIELD 51 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # When we publish to npm, the published files are available in the root 4 | # directory, which allows for a clean include or require of sub-modules. 5 | # 6 | # var language = require('react-apollo/server'); 7 | # 8 | if [ "${npm_command}" != "run-script" ]; then 9 | echo "To ensure that your npm cli has the right credentials, you must run this through 'npm run deploy' instead of yarn." >&2 10 | exit 1; 11 | fi 12 | 13 | # Clear the built output 14 | rm -rf ./lib 15 | 16 | # Compile new files 17 | npm run build 18 | 19 | # Make sure the ./npm directory is empty 20 | rm -rf ./npm 21 | mkdir ./npm 22 | 23 | # Copy all files from ./lib to /npm 24 | cd ./lib && cp -r ./ ../npm/ 25 | # Copy also the umd bundle with the source map file 26 | cp bundle.umd.js ../npm/ && cp bundle.umd.js.map ../npm/ 27 | 28 | # Back to the root directory 29 | cd ../ 30 | 31 | # Ensure a vanilla package.json before deploying so other tools do not interpret 32 | # The built output as requiring any further transformation. 33 | node -e "var package = require('./package.json'); \ 34 | delete package.babel; \ 35 | delete package[\"lint-staged\"]; \ 36 | delete package.jest; \ 37 | delete package.bundlesize; \ 38 | delete package.scripts; \ 39 | delete package.options; \ 40 | package.main = 'bundle.umd.js'; \ 41 | package.browser = 'bundle.umd.js'; \ 42 | package.module = 'index.js'; \ 43 | package['jsnext:main'] = 'index.js'; \ 44 | package['react-native'] = 'index.js'; \ 45 | package.typings = 'index.d.ts'; \ 46 | var origVersion = 'local'; 47 | var fs = require('fs'); \ 48 | fs.writeFileSync('./npm/package.json', JSON.stringify(package, null, 2)); \ 49 | " 50 | 51 | # Copy few more files to ./npm 52 | cp README.md npm/ 53 | cp LICENSE npm/ 54 | cp schema.graphql npm/ 55 | 56 | echo "deploying to npm…" 57 | (cd npm && npm publish) || (>&2 echo "If this failed with ENEEDAUTH, remember that 'yarn deploy' won't work because yarn hot-patches npm's registry to yarn pkg.com") 58 | -------------------------------------------------------------------------------- /scripts/docs_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################################################### 3 | ## File: ./scripts/docs_check.sh 4 | ## Purpose: Alerts you if the docs are different from apollo-link! 5 | ############################################################################### 6 | 7 | # Create dir if needed 8 | mkdir -p node_modules/apollo-link 9 | 10 | cd node_modules/apollo-link 11 | git clone git@github.com:apollographql/apollo-link.git repo || true # Don't fail if repo already exists 12 | cd repo 13 | 14 | git reset --hard 15 | git checkout master 16 | git pull 17 | 18 | diff ../../../docs/rest.md ./docs/source/links/rest.md 19 | error=$? 20 | if [ $error -eq 0 ] 21 | then 22 | echo "Docs are in sync!" 23 | exit 0 24 | else 25 | echo "" 26 | echo "Docs have differences. You should yarn docs:push or yarn docs:pull them!" 27 | exit 1 28 | fi -------------------------------------------------------------------------------- /scripts/docs_pull.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################################################### 3 | ## File: ./scripts/docs_pull.sh 4 | ## Purpose: Synchronizes docs by pulling them from apollo-link into this repo 5 | ## Dependencies: 6 | ## - Expects `yarn docs:check` to have been called right before this! 7 | ############################################################################### 8 | 9 | cp node_modules/apollo-link/repo/docs/source/links/rest.md docs/rest.md || echo "Error: did you fail to call yarn docs:check first?" 10 | 11 | git status 12 | -------------------------------------------------------------------------------- /scripts/docs_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################################################### 3 | ## File: ./scripts/docs_push.sh 4 | ## Purpose: Synchronizes docs by pushing them to apollo-link 5 | ## Dependencies: 6 | ## - Expects `yarn docs:check` to have been called right before this! 7 | ############################################################################### 8 | 9 | cp docs/rest.md node_modules/apollo-link/repo/docs/source/links/rest.md 10 | error=$? 11 | 12 | if [[ $error -eq 0 ]] 13 | then 14 | echo "Docs successfully pushed! The repo can be located at:" 15 | echo "cd node_modules/apollo-link/repo" 16 | echo -n "cd node_modules/apollo-link/repo; git checkout -b " | pbcopy 17 | else 18 | echo "Error: did you fail to call yarn docs:check first?" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /scripts/jest.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock'); 2 | 3 | // Allow routes to be stacked (fetch-mock @ >= 7.0) -- is buggy in beta.6 4 | require('fetch-mock').config.overwriteRoutes = false; 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { RestLink, PathBuilder } from './restLink'; 2 | -------------------------------------------------------------------------------- /src/restLink.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OperationTypeNode, 3 | OperationDefinitionNode, 4 | FragmentDefinitionNode, 5 | // Query Nodes 6 | DirectiveNode, 7 | DocumentNode, 8 | FieldNode, 9 | SelectionSetNode, 10 | } from 'graphql'; 11 | import { 12 | ApolloLink, 13 | Observable, 14 | Operation, 15 | NextLink, 16 | FetchResult, 17 | } from '@apollo/client/core'; 18 | import { 19 | hasDirectives, 20 | getMainDefinition, 21 | getFragmentDefinitions, 22 | createFragmentMap, 23 | addTypenameToDocument, 24 | FragmentMap, 25 | isField, 26 | isInlineFragment, 27 | resultKeyNameFromField, 28 | checkDocument, 29 | removeDirectivesFromDocument, 30 | } from '@apollo/client/utilities'; 31 | import { graphql } from './utils/graphql'; 32 | import * as qs from 'qs'; 33 | 34 | export type DirectiveInfo = { 35 | [fieldName: string]: { [argName: string]: any }; 36 | }; 37 | 38 | export type ExecInfo = { 39 | isLeaf: boolean; 40 | resultKey: string; 41 | directives: DirectiveInfo; 42 | field: FieldNode; 43 | }; 44 | 45 | export type Resolver = ( 46 | fieldName: string, 47 | rootValue: any, 48 | args: any, 49 | context: any, 50 | info: ExecInfo, 51 | ) => any; 52 | 53 | export namespace RestLink { 54 | export type URI = string; 55 | 56 | export type Endpoint = string; 57 | 58 | export interface EndpointOptions { 59 | uri: Endpoint; 60 | responseTransformer?: ResponseTransformer | null; 61 | } 62 | 63 | export interface Endpoints { 64 | [endpointKey: string]: Endpoint | EndpointOptions; 65 | } 66 | 67 | export type Header = string; 68 | export interface HeadersHash { 69 | [headerKey: string]: Header; 70 | } 71 | export type InitializationHeaders = HeadersHash | Headers | string[][]; 72 | 73 | export type HeadersMergePolicy = (...headerGroups: Headers[]) => Headers; 74 | 75 | export interface FieldNameNormalizer { 76 | (fieldName: string, keypath?: string[]): string; 77 | } 78 | 79 | export interface TypePatcherContext { 80 | resolverParams: { 81 | fieldName: string; 82 | root: any; 83 | args: any; 84 | context: RequestContext; 85 | info: ExecInfo; 86 | }; 87 | } 88 | 89 | /** injects __typename using user-supplied code */ 90 | export interface FunctionalTypePatcher { 91 | ( 92 | data: any, 93 | outerType: string, 94 | patchDeeper: FunctionalTypePatcher, 95 | context: TypePatcherContext, 96 | ): any; 97 | } 98 | /** Table of mappers that help inject __typename per type described therein */ 99 | export interface TypePatcherTable { 100 | [typename: string]: FunctionalTypePatcher; 101 | } 102 | 103 | export interface SerializedBody { 104 | body: any; 105 | headers: InitializationHeaders; 106 | } 107 | 108 | export interface Serializer { 109 | (data: any, headers: Headers): SerializedBody; 110 | } 111 | 112 | export interface Serializers { 113 | [bodySerializer: string]: Serializer; 114 | } 115 | 116 | export type CustomFetch = ( 117 | request: RequestInfo, 118 | init: RequestInit, 119 | ) => Promise; 120 | 121 | export type ResponseTransformer = (data: any, typeName: string) => any; 122 | 123 | export interface RestLinkHelperProps { 124 | /** Arguments passed in via normal graphql parameters */ 125 | args: { [key: string]: any }; 126 | /** Arguments added via @export(as: ) directives */ 127 | exportVariables: { [key: string]: any }; 128 | /** Arguments passed directly to @rest(params: ) */ 129 | // params: { [key: string]: any }; 130 | /** Apollo Context */ 131 | context: { [key: string]: any }; 132 | /** All arguments passed to the `@rest(...)` directive */ 133 | '@rest': { [key: string]: any }; 134 | } 135 | export interface PathBuilderProps extends RestLinkHelperProps { 136 | replacer: (opts: RestLinkHelperProps) => string; 137 | } 138 | 139 | /** 140 | * Used for any Error from the server when requests: 141 | * - terminate with HTTP Status >= 300 142 | * - and the response contains no data or errors 143 | */ 144 | export type ServerError = Error & { 145 | response: Response; 146 | result: any; 147 | statusCode: number; 148 | }; 149 | 150 | export type Options = { 151 | /** 152 | * The URI to use when fetching operations. 153 | * 154 | * Optional if endpoints provides a default. 155 | */ 156 | uri?: URI; 157 | 158 | /** 159 | * A root endpoint (uri) to apply paths to or a map of endpoints. 160 | */ 161 | endpoints?: Endpoints; 162 | 163 | /** 164 | * An object representing values to be sent as headers on the request. 165 | */ 166 | headers?: InitializationHeaders; 167 | 168 | /** 169 | * A function that takes the response field name and converts it into a GraphQL compliant name 170 | * 171 | * @note This is called *before* @see typePatcher so that it happens after 172 | * optional-field-null-insertion. 173 | */ 174 | fieldNameNormalizer?: FieldNameNormalizer; 175 | 176 | /** 177 | * A function that takes a GraphQL-compliant field name and converts it back into an endpoint-specific name 178 | * Can be overridden at the mutation-call-site (in the rest-directive). 179 | */ 180 | fieldNameDenormalizer?: FieldNameNormalizer; 181 | 182 | /** 183 | * Structure to allow you to specify the __typename when you have nested objects in your REST response! 184 | * 185 | * If you want to force Required Properties, you can throw an error in your patcher, 186 | * or `delete` a field from the data response provided to your typePatcher function! 187 | * 188 | * @note: This is called *after* @see fieldNameNormalizer because that happens 189 | * after optional-nulls insertion, and those would clobber normalized names. 190 | * 191 | * @warning: We're not thrilled with this API, and would love a better alternative before we get to 1.0.0 192 | * Please see proposals considered in https://github.com/apollographql/apollo-link-rest/issues/48 193 | * And consider submitting alternate solutions to the problem! 194 | */ 195 | typePatcher?: TypePatcherTable; 196 | 197 | /** 198 | * The credentials policy you want to use for the fetch call. 199 | */ 200 | credentials?: 'omit' | 'same-origin' | 'include'; 201 | 202 | /** 203 | * Use a custom fetch to handle REST calls. 204 | */ 205 | customFetch?: CustomFetch; 206 | 207 | /** 208 | * Add serializers that will serialize the body before it is emitted and will pass on 209 | * headers to update the request. 210 | */ 211 | bodySerializers?: Serializers; 212 | 213 | /** 214 | * Set the default serializer for the link 215 | * @default JSON serialization 216 | */ 217 | defaultSerializer?: Serializer; 218 | 219 | /** 220 | * Parse the response body of an HTTP request into the format that Apollo expects. 221 | */ 222 | responseTransformer?: ResponseTransformer; 223 | }; 224 | 225 | /** @rest(...) Directive Options */ 226 | export interface DirectiveOptions { 227 | /** 228 | * What HTTP method to use. 229 | * @default `GET` 230 | */ 231 | method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; 232 | /** What GraphQL type to name the response */ 233 | type?: string; 234 | /** 235 | * What path (including query) to use 236 | * - @optional if you provide @see DirectiveOptions.pathBuilder 237 | */ 238 | path?: string; 239 | /** 240 | * What endpoint to select from the map of endpoints available to this link. 241 | * @default `RestLink.endpoints[DEFAULT_ENDPOINT_KEY]` 242 | */ 243 | endpoint?: string; 244 | /** 245 | * Function that constructs a request path out of the Environmental 246 | * state when processing this @rest(...) call. 247 | * 248 | * - @optional if you provide: @see DirectiveOptions.path 249 | * - **note**: providing this function means it's your responsibility to call 250 | * encodeURIComponent directly if needed! 251 | * 252 | * Warning: This is an Advanced API and we are looking for syntactic & ergonomics feedback. 253 | */ 254 | pathBuilder?: (props: PathBuilderProps) => string; 255 | /** 256 | * Optional method that constructs a RequestBody out of the Environmental state 257 | * when processing this @rest(...) call. 258 | * @default function that extracts the bodyKey from the args. 259 | * 260 | * Warning: This is an Advanced API and we are looking for syntactic & ergonomics feedback. 261 | */ 262 | bodyBuilder?: (props: RestLinkHelperProps) => object; 263 | /** 264 | * Optional field that defines the name of the env var to extract and use as the body 265 | * @default "input" 266 | * @see https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97 267 | */ 268 | bodyKey?: string; 269 | 270 | /** 271 | * Optional serialization function or a key that will be used look up the serializer to serialize the request body before transport. 272 | * @default if null will fallback to the default serializer 273 | */ 274 | bodySerializer?: RestLink.Serializer | string; 275 | 276 | /** 277 | * A per-request name denormalizer, this permits special endpoints to have their 278 | * field names remapped differently from the default. 279 | * @default Uses RestLink.fieldNameDenormalizer 280 | */ 281 | fieldNameDenormalizer?: RestLink.FieldNameNormalizer; 282 | /** 283 | * A per-request name normalizer, this permits special endpoints to have their 284 | * field names remapped differently from the default. 285 | * @default Uses RestLink.fieldNameDenormalizer 286 | */ 287 | fieldNameNormalizer?: RestLink.FieldNameNormalizer; 288 | /** 289 | * A method to allow insertion of __typename deep in response objects 290 | */ 291 | typePatcher?: RestLink.FunctionalTypePatcher; 292 | } 293 | } 294 | 295 | const popOneSetOfArrayBracketsFromTypeName = (typename: string): string => { 296 | const noSpace = typename.replace(/\s/g, ''); 297 | const sansOneBracketPair = noSpace.replace( 298 | /\[(.*)\]/, 299 | (str, matchStr, offset, fullStr) => { 300 | return ( 301 | ((matchStr != null && matchStr.length) > 0 ? matchStr : null) || noSpace 302 | ); 303 | }, 304 | ); 305 | return sansOneBracketPair; 306 | }; 307 | 308 | const addTypeNameToResult = ( 309 | result: any[] | object, 310 | __typename: string, 311 | typePatcher: RestLink.FunctionalTypePatcher, 312 | typePatcherContext: RestLink.TypePatcherContext, 313 | ): any[] | object => { 314 | if (Array.isArray(result)) { 315 | const fixedTypename = popOneSetOfArrayBracketsFromTypeName(__typename); 316 | // Recursion needed for multi-dimensional arrays 317 | return result.map(e => 318 | addTypeNameToResult(e, fixedTypename, typePatcher, typePatcherContext), 319 | ); 320 | } 321 | if ( 322 | null == result || 323 | typeof result === 'number' || 324 | typeof result === 'boolean' || 325 | typeof result === 'string' 326 | ) { 327 | return result; 328 | } 329 | return typePatcher(result, __typename, typePatcher, typePatcherContext); 330 | }; 331 | 332 | const quickFindRestDirective = (field: FieldNode): DirectiveNode | null => { 333 | if (field.directives && field.directives.length) { 334 | return field.directives.find(directive => 'rest' === directive.name.value); 335 | } 336 | return null; 337 | }; 338 | /** 339 | * The way graphql works today, it doesn't hand us the AST tree for our query, it hands us the ROOT 340 | * This method searches for REST-directive-attached nodes that are named to match this query. 341 | * 342 | * A little bit of wasted compute, but alternative would be a patch in graphql-anywhere. 343 | * 344 | * @param resultKey SearchKey for REST directive-attached item matching this sub-query 345 | * @param current current node in the REST-JSON-response 346 | * @param mainDefinition Parsed Query Definition 347 | * @param fragmentMap Map of Named Fragments 348 | * @param currentSelectionSet Current selection set we're filtering by 349 | */ 350 | function findRestDirectivesThenInsertNullsForOmittedFields( 351 | resultKey: string, 352 | current: any[] | object, // currentSelectionSet starts at root, so wait until we're inside a Field tagged with an @rest directive to activate! 353 | mainDefinition: OperationDefinitionNode | FragmentDefinitionNode, 354 | fragmentMap: FragmentMap, 355 | currentSelectionSet: SelectionSetNode, 356 | ): any[] | object { 357 | if ( 358 | currentSelectionSet == null || 359 | null == current || 360 | typeof current === 'number' || 361 | typeof current === 'boolean' || 362 | typeof current === 'string' 363 | ) { 364 | return current; 365 | } 366 | currentSelectionSet.selections.forEach(node => { 367 | if (isInlineFragment(node)) { 368 | findRestDirectivesThenInsertNullsForOmittedFields( 369 | resultKey, 370 | current, 371 | mainDefinition, 372 | fragmentMap, 373 | node.selectionSet, 374 | ); 375 | } else if (node.kind === 'FragmentSpread') { 376 | const fragment = fragmentMap[node.name.value]; 377 | findRestDirectivesThenInsertNullsForOmittedFields( 378 | resultKey, 379 | current, 380 | mainDefinition, 381 | fragmentMap, 382 | fragment.selectionSet, 383 | ); 384 | } else if (isField(node)) { 385 | const name = resultKeyNameFromField(node); 386 | if (name === resultKey && quickFindRestDirective(node) != null) { 387 | // Jackpot! We found our selectionSet! 388 | insertNullsForAnyOmittedFields( 389 | current, 390 | mainDefinition, 391 | fragmentMap, 392 | node.selectionSet, 393 | ); 394 | } else { 395 | findRestDirectivesThenInsertNullsForOmittedFields( 396 | resultKey, 397 | current, 398 | mainDefinition, 399 | fragmentMap, 400 | node.selectionSet, 401 | ); 402 | } 403 | } else { 404 | // This will give a TypeScript build-time error if you did something wrong or the AST changes! 405 | return ((node: never): never => { 406 | throw new Error('Unhandled Node Type in SelectionSetNode.selections'); 407 | })(node); 408 | } 409 | }); 410 | // Return current to have our result pass to next link in async promise chain! 411 | return current; 412 | } 413 | /** 414 | * Recursively walks a handed object in parallel with the Query SelectionSet, 415 | * and inserts `null` for any field that is missing from the response. 416 | * 417 | * This is needed because ApolloClient will throw an error automatically if it's 418 | * missing -- effectively making all of rest-link's selections implicitly non-optional. 419 | * 420 | * If you want to implement required fields, you need to use typePatcher to *delete* 421 | * fields when they're null and you want the query to fail instead. 422 | * 423 | * @param current Current object we're patching 424 | * @param mainDefinition Parsed Query Definition 425 | * @param fragmentMap Map of Named Fragments 426 | * @param currentSelectionSet Current selection set we're filtering by 427 | */ 428 | function insertNullsForAnyOmittedFields( 429 | current: any[] | object, // currentSelectionSet starts at root, so wait until we're inside a Field tagged with an @rest directive to activate! 430 | mainDefinition: OperationDefinitionNode | FragmentDefinitionNode, 431 | fragmentMap: FragmentMap, 432 | currentSelectionSet: SelectionSetNode, 433 | ): void { 434 | if ( 435 | null == current || 436 | typeof current === 'number' || 437 | typeof current === 'boolean' || 438 | typeof current === 'string' 439 | ) { 440 | return; 441 | } 442 | if (Array.isArray(current)) { 443 | // If our current value is an array, process our selection set for each entry. 444 | current.forEach(c => 445 | insertNullsForAnyOmittedFields( 446 | c, 447 | mainDefinition, 448 | fragmentMap, 449 | currentSelectionSet, 450 | ), 451 | ); 452 | return; 453 | } 454 | currentSelectionSet.selections.forEach(node => { 455 | if (isInlineFragment(node)) { 456 | insertNullsForAnyOmittedFields( 457 | current, 458 | mainDefinition, 459 | fragmentMap, 460 | node.selectionSet, 461 | ); 462 | } else if (node.kind === 'FragmentSpread') { 463 | const fragment = fragmentMap[node.name.value]; 464 | insertNullsForAnyOmittedFields( 465 | current, 466 | mainDefinition, 467 | fragmentMap, 468 | fragment.selectionSet, 469 | ); 470 | } else if (isField(node)) { 471 | const value = current[node.name.value]; 472 | if (node.name.value === '__typename') { 473 | // Don't mess with special fields like __typename 474 | } else if (typeof value === 'undefined') { 475 | // Patch in a null where the field would have been marked as missing 476 | current[node.name.value] = null; 477 | } else if ( 478 | value != null && 479 | typeof value === 'object' && 480 | node.selectionSet != null 481 | ) { 482 | insertNullsForAnyOmittedFields( 483 | value, 484 | mainDefinition, 485 | fragmentMap, 486 | node.selectionSet, 487 | ); 488 | } else { 489 | // Other types (string, number) do not need recursive patching! 490 | } 491 | } else { 492 | // This will give a TypeScript build-time error if you did something wrong or the AST changes! 493 | return ((node: never): never => { 494 | throw new Error('Unhandled Node Type in SelectionSetNode.selections'); 495 | })(node); 496 | } 497 | }); 498 | } 499 | 500 | const getEndpointOptions = ( 501 | endpoints: RestLink.Endpoints, 502 | endpoint: RestLink.Endpoint, 503 | ): RestLink.EndpointOptions => { 504 | const result = 505 | endpoints[endpoint || DEFAULT_ENDPOINT_KEY] || 506 | endpoints[DEFAULT_ENDPOINT_KEY]; 507 | 508 | if (typeof result === 'string') { 509 | return { uri: result }; 510 | } 511 | 512 | return { 513 | responseTransformer: null, 514 | ...result, 515 | }; 516 | }; 517 | 518 | /** Replaces params in the path, keyed by colons */ 519 | const replaceLegacyParam = ( 520 | endpoint: string, 521 | name: string, 522 | value: string, 523 | ): string => { 524 | if (value === undefined || name === undefined) { 525 | return endpoint; 526 | } 527 | return endpoint.replace(`:${name}`, value); 528 | }; 529 | 530 | /** Internal Tool that Parses Paths for RestLink -- This API should be considered experimental */ 531 | export class PathBuilder { 532 | /** For accelerating the replacement of paths that are used a lot */ 533 | private static cache: { 534 | [path: string]: (props: RestLink.PathBuilderProps) => string; 535 | } = {}; 536 | /** Table to limit the amount of nagging (due to probable API Misuse) we do to once per path per launch */ 537 | private static warnTable: { [key: string]: true } = {}; 538 | /** Regexp that finds things that are eligible for variable replacement */ 539 | private static argReplacement = /({[._a-zA-Z0-9]*})/; 540 | 541 | static replacerForPath( 542 | path: string, 543 | ): (props: RestLink.PathBuilderProps) => string { 544 | if (path in PathBuilder.cache) { 545 | return PathBuilder.cache[path]; 546 | } 547 | 548 | const queryOrigStartIndex = path.indexOf('?'); 549 | const pathBits = path.split(PathBuilder.argReplacement); 550 | 551 | const chunkActions: Array< 552 | | true // We're enabling the qs-encoder 553 | | string // This is a raw string bit, don't mess with it 554 | | ((props: RestLink.RestLinkHelperProps, useQSEncoder: boolean) => string) 555 | > = []; 556 | 557 | let hasBegunQuery = false; 558 | pathBits.reduce((processedCount, bit) => { 559 | if (bit === '' || bit === '{}') { 560 | // Empty chunk, do nothing 561 | return processedCount + bit.length; 562 | } 563 | const nextIndex = processedCount + bit.length; 564 | if (bit[0] === '{' && bit[bit.length - 1] === '}') { 565 | // Replace some args! 566 | const _keyPath = bit.slice(1, bit.length - 1).split('.'); 567 | 568 | chunkActions.push( 569 | (props: RestLink.RestLinkHelperProps, useQSEncoder: boolean) => { 570 | try { 571 | const value = PathBuilderLookupValue(props, _keyPath); 572 | if ( 573 | !useQSEncoder || 574 | (typeof value !== 'object' || value == null) 575 | ) { 576 | return String(value); 577 | } else { 578 | return qs.stringify(value); 579 | } 580 | } catch (e) { 581 | const key = [path, _keyPath.join('.')].join('|'); 582 | if (!(key in PathBuilder.warnTable)) { 583 | console.warn( 584 | 'Warning: RestLink caught an error while unpacking', 585 | key, 586 | "This tends to happen if you forgot to pass a parameter needed for creating an @rest(path, or if RestLink was configured to deeply unpack a path parameter that wasn't provided. This message will only log once per detected instance. Trouble-shooting hint: check @rest(path: and the variables provided to this query.", 587 | ); 588 | PathBuilder.warnTable[key] = true; 589 | } 590 | return ''; 591 | } 592 | }, 593 | ); 594 | } else { 595 | chunkActions.push(bit); 596 | if (!hasBegunQuery && nextIndex >= queryOrigStartIndex) { 597 | hasBegunQuery = true; 598 | chunkActions.push(true); 599 | } 600 | } 601 | return nextIndex; 602 | }, 0); 603 | 604 | const result: (props: RestLink.PathBuilderProps) => string = props => { 605 | let hasEnteredQuery = false; 606 | const tmp = chunkActions.reduce((accumulator: string, action): string => { 607 | if (typeof action === 'string') { 608 | return accumulator + action; 609 | } else if (typeof action === 'boolean') { 610 | hasEnteredQuery = true; 611 | return accumulator; 612 | } else { 613 | return accumulator + action(props, hasEnteredQuery); 614 | } 615 | }, '') as string; 616 | return tmp; 617 | }; 618 | return (PathBuilder.cache[path] = result); 619 | } 620 | } 621 | 622 | /** Private Helper Function */ 623 | function PathBuilderLookupValue(tmp: object, keyPath: string[]) { 624 | if (keyPath.length === 0) { 625 | return tmp; 626 | } 627 | const remainingKeyPath = [...keyPath]; // Copy before mutating 628 | const key = remainingKeyPath.shift(); 629 | return PathBuilderLookupValue(tmp[key], remainingKeyPath); 630 | } 631 | 632 | /** 633 | * Some keys should be passed through transparently without normalizing/de-normalizing 634 | */ 635 | const noMangleKeys = ['__typename']; 636 | 637 | /** Trivial globalThis polyfill that falls-back to our previous global object in case people had polyfilled that */ 638 | const globalScope = (typeof globalThis === 'object' && globalThis) || global; 639 | 640 | /** Recursively descends the provided object tree and converts all the keys */ 641 | const convertObjectKeys = ( 642 | object: object, 643 | __converter: RestLink.FieldNameNormalizer, 644 | keypath: string[] = [], 645 | ): object => { 646 | let converter: RestLink.FieldNameNormalizer = null; 647 | if (__converter.length != 2) { 648 | converter = (name, keypath) => { 649 | return __converter(name); 650 | }; 651 | } else { 652 | converter = __converter; 653 | } 654 | 655 | if (Array.isArray(object)) { 656 | return object.map((o, index) => 657 | convertObjectKeys(o, converter, [...keypath, String(index)]), 658 | ); 659 | } 660 | 661 | if ( 662 | object == null || 663 | typeof object !== 'object' || 664 | object.constructor !== Object 665 | ) { 666 | // Object is a scalar or null / undefined => no keys to convert! 667 | return object; 668 | } 669 | 670 | // FileList/File are only available in some browser contexts 671 | // Notably: *not available* in react-native. 672 | if ( 673 | ((globalScope as any).FileList && object instanceof FileList) || 674 | ((globalScope as any).File && object instanceof File) 675 | ) { 676 | // Object is a FileList or File object => no keys to convert! 677 | return object; 678 | } 679 | 680 | return Object.keys(object).reduce((acc: any, key: string) => { 681 | let value = object[key]; 682 | 683 | if (noMangleKeys.indexOf(key) !== -1) { 684 | acc[key] = value; 685 | return acc; 686 | } 687 | 688 | const nestedKeyPath = [...keypath, key]; 689 | acc[converter(key, nestedKeyPath)] = convertObjectKeys( 690 | value, 691 | converter, 692 | nestedKeyPath, 693 | ); 694 | return acc; 695 | }, {}); 696 | }; 697 | 698 | const noOpNameNormalizer: RestLink.FieldNameNormalizer = (name: string) => { 699 | return name; 700 | }; 701 | 702 | /** 703 | * Helper that makes sure our headers are of the right type to pass to Fetch 704 | */ 705 | export const normalizeHeaders = ( 706 | headers: RestLink.InitializationHeaders, 707 | ): Headers => { 708 | // Make sure that our headers object is of the right type 709 | if (headers instanceof Headers) { 710 | return headers; 711 | } else { 712 | return new Headers(headers || {}); 713 | } 714 | }; 715 | 716 | /** 717 | * Returns a new Headers Group that contains all the headers. 718 | * - If there are duplicates, they will be in the returned header set multiple times! 719 | */ 720 | export const concatHeadersMergePolicy: RestLink.HeadersMergePolicy = ( 721 | ...headerGroups: Headers[] 722 | ): Headers => { 723 | return headerGroups.reduce((accumulator, current) => { 724 | if (!current) { 725 | return accumulator; 726 | } 727 | if (!current.forEach) { 728 | current = normalizeHeaders(current); 729 | } 730 | current.forEach((value, key) => { 731 | accumulator.append(key, value); 732 | }); 733 | 734 | return accumulator; 735 | }, new Headers()); 736 | }; 737 | 738 | /** 739 | * This merge policy deletes any matching headers from the link's default headers. 740 | * - Pass headersToOverride array & a headers arg to context and this policy will automatically be selected. 741 | */ 742 | export const overrideHeadersMergePolicy = ( 743 | linkHeaders: Headers, 744 | headersToOverride: string[], 745 | requestHeaders: Headers | null, 746 | ): Headers => { 747 | const result = new Headers(); 748 | linkHeaders.forEach((value, key) => { 749 | if (headersToOverride.indexOf(key) !== -1) { 750 | return; 751 | } 752 | result.append(key, value); 753 | }); 754 | return concatHeadersMergePolicy(result, requestHeaders || new Headers()); 755 | }; 756 | export const overrideHeadersMergePolicyHelper = overrideHeadersMergePolicy; // Deprecated name 757 | 758 | const makeOverrideHeadersMergePolicy = ( 759 | headersToOverride: string[], 760 | ): RestLink.HeadersMergePolicy => { 761 | return (linkHeaders, requestHeaders) => { 762 | return overrideHeadersMergePolicy( 763 | linkHeaders, 764 | headersToOverride, 765 | requestHeaders, 766 | ); 767 | }; 768 | }; 769 | 770 | const SUPPORTED_HTTP_VERBS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; 771 | 772 | export const validateRequestMethodForOperationType = ( 773 | method: string, 774 | operationType: OperationTypeNode, 775 | ): void => { 776 | switch (operationType) { 777 | case 'query': 778 | if (SUPPORTED_HTTP_VERBS.indexOf(method.toUpperCase()) !== -1) { 779 | return; 780 | } 781 | throw new Error( 782 | `A "query" operation can only support "GET" requests but got "${method}".`, 783 | ); 784 | case 'mutation': 785 | if (SUPPORTED_HTTP_VERBS.indexOf(method.toUpperCase()) !== -1) { 786 | return; 787 | } 788 | throw new Error('"mutation" operations do not support that HTTP-verb'); 789 | case 'subscription': 790 | throw new Error('A "subscription" operation is not supported yet.'); 791 | default: 792 | const _exhaustiveCheck: never = operationType; 793 | return _exhaustiveCheck; 794 | } 795 | }; 796 | 797 | /** 798 | * Utility to build & throw a JS Error from a "failed" REST-response 799 | * @param response: HTTP Response object for this request 800 | * @param result: Promise that will render the body of the response 801 | * @param message: Human-facing error message 802 | */ 803 | const rethrowServerSideError = ( 804 | response: Response, 805 | result: any, 806 | message: string, 807 | ) => { 808 | const error = new Error(message) as RestLink.ServerError; 809 | 810 | error.response = response; 811 | error.statusCode = response.status; 812 | error.result = result; 813 | 814 | throw error; 815 | }; 816 | 817 | /** Apollo-Link getContext, provided from the user & mutated by upstream links */ 818 | interface LinkChainContext { 819 | /** Credentials Policy for Fetch */ 820 | credentials?: RequestCredentials | null; 821 | 822 | /** Headers the user wants to set on this request. See also headersMergePolicy */ 823 | headers?: RestLink.InitializationHeaders | null; 824 | 825 | /** Will default to concatHeadersMergePolicy unless headersToOverride is set */ 826 | headersMergePolicy?: RestLink.HeadersMergePolicy | null; 827 | 828 | /** List of headers to override, passing this will swap headersMergePolicy if necessary */ 829 | headersToOverride?: string[] | null; 830 | 831 | /** An array of the responses from each fetched URL, useful for accessing headers in earlier links */ 832 | restResponses?: Response[]; 833 | } 834 | 835 | /** Context passed via graphql() to our resolver */ 836 | interface RequestContext { 837 | /** Headers the user wants to set on this request. See also headersMergePolicy */ 838 | headers: Headers; 839 | 840 | /** Credentials Policy for Fetch */ 841 | credentials?: RequestCredentials | null; 842 | 843 | /** Exported variables fulfilled in this request, using @export(as:). They are stored keyed by node to support deeply nested structures with exports at multiple levels */ 844 | exportVariablesByNode: Map; 845 | 846 | endpoints: RestLink.Endpoints; 847 | customFetch: RestLink.CustomFetch; 848 | operationType: OperationTypeNode; 849 | fieldNameNormalizer: RestLink.FieldNameNormalizer; 850 | fieldNameDenormalizer: RestLink.FieldNameNormalizer; 851 | mainDefinition: OperationDefinitionNode | FragmentDefinitionNode; 852 | fragmentDefinitions: FragmentDefinitionNode[]; 853 | typePatcher: RestLink.FunctionalTypePatcher; 854 | serializers: RestLink.Serializers; 855 | responseTransformer: RestLink.ResponseTransformer; 856 | 857 | /** An array of the responses from each fetched URL */ 858 | responses: Response[]; 859 | } 860 | 861 | const addTypeToNode = (node, typename) => { 862 | if (node === null || node === undefined || typeof node !== 'object') { 863 | return node; 864 | } 865 | 866 | if (!Array.isArray(node)) { 867 | node['__typename'] = typename; 868 | return node; 869 | } 870 | 871 | return node.map(item => { 872 | return addTypeToNode(item, typename); 873 | }); 874 | }; 875 | 876 | const resolver: Resolver = async ( 877 | fieldName: string, 878 | root: any, 879 | args: any, 880 | context: RequestContext, 881 | info: ExecInfo, 882 | ) => { 883 | const { directives, isLeaf, resultKey } = info; 884 | const { exportVariablesByNode } = context; 885 | 886 | const exportVariables = exportVariablesByNode.get(root) || {}; 887 | 888 | /** creates a copy of this node's export variables for its child nodes. iterates over array results to provide for each child. returns the passed result. */ 889 | const copyExportVariables = (result: T): T => { 890 | if (result instanceof Array) { 891 | result.forEach(copyExportVariables); 892 | } else { 893 | // export variables are stored keyed on the node they are for 894 | exportVariablesByNode.set(result, { ...exportVariables }); 895 | } 896 | 897 | return result; 898 | }; 899 | 900 | // Support GraphQL Aliases! 901 | const aliasedNode = (root || {})[resultKey]; 902 | const preAliasingNode = (root || {})[fieldName]; 903 | 904 | if (root && directives && directives.export) { 905 | // @export(as:) is only supported with apollo-link-rest at this time 906 | // so use the preAliasingNode as we're responsible for implementing aliasing! 907 | exportVariables[directives.export.as] = preAliasingNode; 908 | } 909 | 910 | const isATypeCall = directives && directives.type; 911 | 912 | if (!isLeaf && isATypeCall) { 913 | // @type(name: ) is only supported inside apollo-link-rest at this time 914 | // so use the preAliasingNode as we're responsible for implementing aliasing! 915 | // Also: exit early, since @type(name: ) && @rest() can't both exist on the same node. 916 | if (directives.rest) { 917 | throw new Error( 918 | 'Invalid use of @type(name: ...) directive on a call that also has @rest(...)', 919 | ); 920 | } 921 | copyExportVariables(preAliasingNode); 922 | return addTypeToNode(preAliasingNode, directives.type.name); 923 | } 924 | 925 | const isNotARestCall = !directives || !directives.rest; 926 | if (isNotARestCall) { 927 | // This is not tagged with @rest() 928 | // This might not belong to us so return the aliasNode version preferentially 929 | return copyExportVariables(aliasedNode || preAliasingNode); 930 | } 931 | const { 932 | credentials, 933 | endpoints, 934 | headers, 935 | customFetch, 936 | operationType, 937 | typePatcher, 938 | mainDefinition, 939 | fragmentDefinitions, 940 | fieldNameNormalizer: linkLevelNameNormalizer, 941 | fieldNameDenormalizer: linkLevelNameDenormalizer, 942 | serializers, 943 | responseTransformer, 944 | } = context; 945 | 946 | const fragmentMap = createFragmentMap(fragmentDefinitions); 947 | 948 | let { 949 | path, 950 | endpoint, 951 | pathBuilder, 952 | } = directives.rest as RestLink.DirectiveOptions; 953 | 954 | const endpointOption = getEndpointOptions(endpoints, endpoint); 955 | const neitherPathsProvided = path == null && pathBuilder == null; 956 | 957 | if (neitherPathsProvided) { 958 | throw new Error( 959 | `One of ("path" | "pathBuilder") must be set in the @rest() directive. This request had neither, please add one`, 960 | ); 961 | } 962 | if (!pathBuilder) { 963 | if (!path.includes(':')) { 964 | // Colons are the legacy route, and aren't uri encoded anyhow. 965 | pathBuilder = PathBuilder.replacerForPath(path); 966 | } else { 967 | console.warn( 968 | "Deprecated: '@rest(path:' contains a ':' colon, this format will be removed in future versions", 969 | ); 970 | 971 | pathBuilder = ({ 972 | args, 973 | exportVariables, 974 | }: RestLink.PathBuilderProps): string => { 975 | const legacyArgs = { 976 | ...args, 977 | ...exportVariables, 978 | }; 979 | const pathWithParams = Object.keys(legacyArgs).reduce( 980 | (acc, e) => replaceLegacyParam(acc, e, legacyArgs[e]), 981 | path, 982 | ); 983 | if (pathWithParams.includes(':')) { 984 | throw new Error( 985 | 'Missing parameters to run query, specify it in the query params or use ' + 986 | 'an export directive. (If you need to use ":" inside a variable string' + 987 | ' make sure to encode the variables properly using `encodeURIComponent' + 988 | '`. Alternatively see documentation about using pathBuilder.)', 989 | ); 990 | } 991 | return pathWithParams; 992 | }; 993 | } 994 | } 995 | const allParams: RestLink.PathBuilderProps = { 996 | args, 997 | exportVariables, 998 | context, 999 | '@rest': directives.rest, 1000 | replacer: pathBuilder, 1001 | }; 1002 | const pathWithParams = pathBuilder(allParams); 1003 | 1004 | let { 1005 | method, 1006 | type, 1007 | bodyBuilder, 1008 | bodyKey, 1009 | fieldNameDenormalizer: perRequestNameDenormalizer, 1010 | fieldNameNormalizer: perRequestNameNormalizer, 1011 | bodySerializer, 1012 | } = directives.rest as RestLink.DirectiveOptions; 1013 | if (!method) { 1014 | method = 'GET'; 1015 | } 1016 | if (!bodyKey) { 1017 | bodyKey = 'input'; 1018 | } 1019 | 1020 | let body = undefined; 1021 | let overrideHeaders: Headers = undefined; 1022 | if (-1 === ['GET', 'DELETE'].indexOf(method)) { 1023 | // Prepare our body! 1024 | if (!bodyBuilder) { 1025 | // By convention GraphQL recommends mutations having a single argument named "input" 1026 | // https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97 1027 | 1028 | const maybeBody = 1029 | allParams.exportVariables[bodyKey] || 1030 | (allParams.args && allParams.args[bodyKey]); 1031 | if (!maybeBody) { 1032 | throw new Error( 1033 | `[GraphQL ${method} ${operationType} using a REST call without a body]. No \`${bodyKey}\` was detected. Pass bodyKey, or bodyBuilder to the @rest() directive to resolve this.`, 1034 | ); 1035 | } 1036 | 1037 | bodyBuilder = (argsWithExport: object) => { 1038 | return maybeBody; 1039 | }; 1040 | } 1041 | 1042 | body = convertObjectKeys( 1043 | bodyBuilder(allParams), 1044 | perRequestNameDenormalizer || 1045 | linkLevelNameDenormalizer || 1046 | noOpNameNormalizer, 1047 | ); 1048 | 1049 | let serializedBody: RestLink.SerializedBody; 1050 | 1051 | if (typeof bodySerializer === 'string') { 1052 | if (!serializers.hasOwnProperty(bodySerializer)) { 1053 | throw new Error( 1054 | '"bodySerializer" must correspond to configured serializer. ' + 1055 | `Please make sure to specify a serializer called ${bodySerializer} in the "bodySerializers" property of the RestLink.`, 1056 | ); 1057 | } 1058 | serializedBody = serializers[bodySerializer](body, headers); 1059 | } else { 1060 | serializedBody = bodySerializer 1061 | ? bodySerializer(body, headers) 1062 | : serializers[DEFAULT_SERIALIZER_KEY](body, headers); 1063 | } 1064 | 1065 | body = serializedBody.body; 1066 | overrideHeaders = new Headers(serializedBody.headers); 1067 | } 1068 | 1069 | validateRequestMethodForOperationType(method, operationType || 'query'); 1070 | 1071 | const requestParams = { 1072 | method, 1073 | headers: overrideHeaders || headers, 1074 | body: body, 1075 | 1076 | // Only set credentials if they're non-null as some browsers throw an exception: 1077 | // https://github.com/apollographql/apollo-link-rest/issues/121#issuecomment-396049677 1078 | ...(credentials ? { credentials } : {}), 1079 | }; 1080 | const requestUrl = `${endpointOption.uri}${pathWithParams}`; 1081 | 1082 | const response = await (customFetch || fetch)(requestUrl, requestParams); 1083 | context.responses.push(response); 1084 | 1085 | let result; 1086 | if (response.ok) { 1087 | if ( 1088 | response.status === 204 || 1089 | response.headers.get('Content-Length') === '0' 1090 | ) { 1091 | // HTTP-204 means "no-content", similarly Content-Length implies the same 1092 | // This commonly occurs when you POST/PUT to the server, and it acknowledges 1093 | // success, but doesn't return your Resource. 1094 | result = {}; 1095 | } else { 1096 | result = response; 1097 | } 1098 | } else if (response.status === 404) { 1099 | // In a GraphQL context a missing resource should be indicated by 1100 | // a null value rather than throwing a network error 1101 | result = null; 1102 | } else { 1103 | // Default error handling: 1104 | // Throw a JSError, that will be available under the 1105 | // "Network error" category in apollo-link-error 1106 | let parsed: any; 1107 | // responses need to be cloned as they can only be read once 1108 | try { 1109 | parsed = await response.clone().json(); 1110 | } catch (error) { 1111 | // its not json 1112 | parsed = await response.clone().text(); 1113 | } 1114 | rethrowServerSideError( 1115 | response, 1116 | parsed, 1117 | `Response not successful: Received status code ${response.status}`, 1118 | ); 1119 | } 1120 | 1121 | const transformer = endpointOption.responseTransformer || responseTransformer; 1122 | 1123 | if (transformer) { 1124 | // A responseTransformer might call something else than json() on the response. 1125 | try { 1126 | result = await transformer(result, type); 1127 | } catch (err) { 1128 | console.warn('An error occurred in a responseTransformer:'); 1129 | throw err; 1130 | } 1131 | } else if (result && result.json) { 1132 | result = await result.json(); 1133 | } 1134 | 1135 | result = convertObjectKeys( 1136 | result, 1137 | perRequestNameNormalizer || linkLevelNameNormalizer || noOpNameNormalizer, 1138 | ); 1139 | 1140 | result = findRestDirectivesThenInsertNullsForOmittedFields( 1141 | resultKey, 1142 | result, 1143 | mainDefinition, 1144 | fragmentMap, 1145 | mainDefinition.selectionSet, 1146 | ); 1147 | 1148 | result = addTypeNameToResult(result, type, typePatcher, { 1149 | resolverParams: { fieldName, root, args, context, info }, 1150 | }); 1151 | return copyExportVariables(result); 1152 | }; 1153 | 1154 | /** 1155 | * Default key to use when the @rest directive omits the "endpoint" parameter. 1156 | */ 1157 | const DEFAULT_ENDPOINT_KEY = ''; 1158 | 1159 | /** 1160 | * Default key to use when the @rest directive omits the "bodySerializers" parameter. 1161 | */ 1162 | const DEFAULT_SERIALIZER_KEY = ''; 1163 | 1164 | const DEFAULT_JSON_SERIALIZER: RestLink.Serializer = ( 1165 | data: any, 1166 | headers: Headers, 1167 | ) => { 1168 | if (!headers.has('content-type')) { 1169 | headers.append('Content-Type', 'application/json'); 1170 | } 1171 | return { 1172 | body: JSON.stringify(data), 1173 | headers: headers, 1174 | }; 1175 | }; 1176 | 1177 | const CONNECTION_REMOVE_CONFIG = { 1178 | test: (directive: DirectiveNode) => directive.name.value === 'rest', 1179 | remove: true, 1180 | }; 1181 | 1182 | /** 1183 | * RestLink is an apollo-link for communicating with REST services using GraphQL on the client-side 1184 | */ 1185 | export class RestLink extends ApolloLink { 1186 | private readonly endpoints: RestLink.Endpoints; 1187 | private readonly headers: Headers; 1188 | private readonly fieldNameNormalizer: RestLink.FieldNameNormalizer; 1189 | private readonly fieldNameDenormalizer: RestLink.FieldNameNormalizer; 1190 | private readonly typePatcher: RestLink.FunctionalTypePatcher; 1191 | private readonly credentials: RequestCredentials; 1192 | private readonly customFetch: RestLink.CustomFetch; 1193 | private readonly serializers: RestLink.Serializers; 1194 | private readonly responseTransformer: RestLink.ResponseTransformer; 1195 | private readonly processedDocuments: Map; 1196 | 1197 | constructor({ 1198 | uri, 1199 | endpoints, 1200 | headers, 1201 | fieldNameNormalizer, 1202 | fieldNameDenormalizer, 1203 | typePatcher, 1204 | customFetch, 1205 | credentials, 1206 | bodySerializers, 1207 | defaultSerializer, 1208 | responseTransformer, 1209 | }: RestLink.Options) { 1210 | super(); 1211 | const fallback = {}; 1212 | fallback[DEFAULT_ENDPOINT_KEY] = uri || ''; 1213 | this.endpoints = Object.assign({}, endpoints || fallback); 1214 | 1215 | if (uri == null && endpoints == null) { 1216 | throw new Error( 1217 | 'A RestLink must be initialized with either 1 uri, or a map of keyed-endpoints', 1218 | ); 1219 | } 1220 | if (uri != null) { 1221 | const currentDefaultURI = (endpoints || {})[DEFAULT_ENDPOINT_KEY]; 1222 | if (currentDefaultURI != null && currentDefaultURI != uri) { 1223 | throw new Error( 1224 | "RestLink was configured with a default uri that doesn't match what's passed in to the endpoints map.", 1225 | ); 1226 | } 1227 | this.endpoints[DEFAULT_ENDPOINT_KEY] = uri; 1228 | } 1229 | 1230 | if (this.endpoints[DEFAULT_ENDPOINT_KEY] == null) { 1231 | console.warn( 1232 | 'RestLink configured without a default URI. All @rest(…) directives must provide an endpoint key!', 1233 | ); 1234 | } 1235 | 1236 | if (typePatcher == null) { 1237 | this.typePatcher = (result, __typename, _2) => { 1238 | return { __typename, ...result }; 1239 | }; 1240 | } else if ( 1241 | !Array.isArray(typePatcher) && 1242 | typeof typePatcher === 'object' && 1243 | Object.keys(typePatcher) 1244 | .map(key => typePatcher[key]) 1245 | .reduce( 1246 | // Make sure all of the values are patcher-functions 1247 | (current, patcher) => current && typeof patcher === 'function', 1248 | true, 1249 | ) 1250 | ) { 1251 | const table: RestLink.TypePatcherTable = typePatcher; 1252 | this.typePatcher = ( 1253 | data: any, 1254 | outerType: string, 1255 | patchDeeper: RestLink.FunctionalTypePatcher, 1256 | context: RestLink.TypePatcherContext, 1257 | ) => { 1258 | const __typename = data.__typename || outerType; 1259 | if (Array.isArray(data)) { 1260 | return data.map(d => 1261 | patchDeeper(d, __typename, patchDeeper, context), 1262 | ); 1263 | } 1264 | const subPatcher = table[__typename] || (result => result); 1265 | return { 1266 | __typename, 1267 | ...subPatcher(data, __typename, patchDeeper, context), 1268 | }; 1269 | }; 1270 | } else { 1271 | throw new Error( 1272 | 'RestLink was configured with a typePatcher of invalid type!', 1273 | ); 1274 | } 1275 | 1276 | if ( 1277 | bodySerializers && 1278 | bodySerializers.hasOwnProperty(DEFAULT_SERIALIZER_KEY) 1279 | ) { 1280 | console.warn( 1281 | 'RestLink was configured to override the default serializer! This may result in unexpected behavior', 1282 | ); 1283 | } 1284 | 1285 | this.responseTransformer = responseTransformer || null; 1286 | this.fieldNameNormalizer = fieldNameNormalizer || null; 1287 | this.fieldNameDenormalizer = fieldNameDenormalizer || null; 1288 | this.headers = normalizeHeaders(headers); 1289 | this.credentials = credentials || null; 1290 | this.customFetch = customFetch; 1291 | this.serializers = { 1292 | [DEFAULT_SERIALIZER_KEY]: defaultSerializer || DEFAULT_JSON_SERIALIZER, 1293 | ...(bodySerializers || {}), 1294 | }; 1295 | this.processedDocuments = new Map(); 1296 | } 1297 | 1298 | private removeRestSetsFromDocument(query: DocumentNode): DocumentNode { 1299 | const cached = this.processedDocuments.get(query); 1300 | if (cached) return cached; 1301 | 1302 | checkDocument(query); 1303 | 1304 | const docClone = removeDirectivesFromDocument( 1305 | [CONNECTION_REMOVE_CONFIG], 1306 | query, 1307 | ); 1308 | 1309 | this.processedDocuments.set(query, docClone); 1310 | return docClone; 1311 | } 1312 | 1313 | public request( 1314 | operation: Operation, 1315 | forward?: NextLink, 1316 | ): Observable | null { 1317 | const { query, variables, getContext, setContext } = operation; 1318 | const context: LinkChainContext | any = getContext() as any; 1319 | const isRestQuery = hasDirectives(['rest'], query); 1320 | if (!isRestQuery) { 1321 | return forward(operation); 1322 | } 1323 | 1324 | const nonRest = this.removeRestSetsFromDocument(query); 1325 | 1326 | // 1. Use the user's merge policy if any 1327 | let headersMergePolicy: RestLink.HeadersMergePolicy = 1328 | context.headersMergePolicy; 1329 | if ( 1330 | headersMergePolicy == null && 1331 | Array.isArray(context.headersToOverride) 1332 | ) { 1333 | // 2.a. Override just the passed in headers, if user provided that optional array 1334 | headersMergePolicy = makeOverrideHeadersMergePolicy( 1335 | context.headersToOverride, 1336 | ); 1337 | } else if (headersMergePolicy == null) { 1338 | // 2.b Glue the link (default) headers to the request-context headers 1339 | headersMergePolicy = concatHeadersMergePolicy; 1340 | } 1341 | 1342 | const headers = headersMergePolicy(this.headers, context.headers); 1343 | if (!headers.has('Accept')) { 1344 | // Since we assume a json body on successful responses set the Accept 1345 | // header accordingly if it is not provided by the user 1346 | headers.append('Accept', 'application/json'); 1347 | } 1348 | 1349 | const credentials: RequestCredentials = 1350 | context.credentials || this.credentials; 1351 | 1352 | const queryWithTypename = addTypenameToDocument(query); 1353 | 1354 | const mainDefinition = getMainDefinition(query); 1355 | const fragmentDefinitions = getFragmentDefinitions(query); 1356 | 1357 | const operationType: OperationTypeNode = 1358 | (mainDefinition || ({} as any)).operation || 'query'; 1359 | 1360 | const requestContext: RequestContext = { 1361 | headers, 1362 | endpoints: this.endpoints, 1363 | // Provide an empty map for this request's exports to be stuffed into 1364 | exportVariablesByNode: new Map(), 1365 | credentials, 1366 | customFetch: this.customFetch, 1367 | operationType, 1368 | fieldNameNormalizer: this.fieldNameNormalizer, 1369 | fieldNameDenormalizer: this.fieldNameDenormalizer, 1370 | mainDefinition, 1371 | fragmentDefinitions, 1372 | typePatcher: this.typePatcher, 1373 | serializers: this.serializers, 1374 | responses: [], 1375 | responseTransformer: this.responseTransformer, 1376 | }; 1377 | const resolverOptions = {}; 1378 | let obs; 1379 | if (nonRest && forward) { 1380 | operation.query = nonRest; 1381 | obs = forward(operation); 1382 | } else obs = Observable.of({ data: {} }); 1383 | 1384 | return obs.flatMap( 1385 | ({ data, errors }) => 1386 | new Observable(observer => { 1387 | graphql( 1388 | resolver, 1389 | queryWithTypename, 1390 | data, 1391 | requestContext, 1392 | variables, 1393 | resolverOptions, 1394 | ) 1395 | .then(data => { 1396 | setContext({ 1397 | restResponses: (context.restResponses || []).concat( 1398 | requestContext.responses, 1399 | ), 1400 | }); 1401 | observer.next({ data, errors }); 1402 | observer.complete(); 1403 | }) 1404 | .catch(err => { 1405 | if (err.name === 'AbortError') return; 1406 | if (err.result && err.result.errors) { 1407 | observer.next(err.result); 1408 | } 1409 | observer.error(err); 1410 | }); 1411 | }), 1412 | ); 1413 | } 1414 | } 1415 | -------------------------------------------------------------------------------- /src/utils/graphql.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a port of async.ts from the now-deprecated graphql-anywhere 3 | package, which itself was based on the graphql fn from graphql-js. 4 | Original source: https://github.com/apollographql/apollo-client/blob/release-2.x/packages/graphql-anywhere/src/async.ts 5 | 6 | Utils that were previously imported from apollo-utilities can now be imported 7 | from @apollo/client/utilities with the remaining types inlined in restLink.ts. 8 | */ 9 | 10 | import { 11 | DocumentNode, 12 | SelectionSetNode, 13 | FieldNode, 14 | FragmentDefinitionNode, 15 | InlineFragmentNode, 16 | DirectiveNode, 17 | } from 'graphql'; 18 | 19 | import { 20 | getMainDefinition, 21 | getFragmentDefinitions, 22 | createFragmentMap, 23 | shouldInclude, 24 | isField, 25 | isInlineFragment, 26 | resultKeyNameFromField, 27 | argumentsObjectFromField, 28 | FragmentMap, 29 | } from '@apollo/client/utilities'; 30 | 31 | import { DirectiveInfo, ExecInfo, Resolver } from '../restLink'; 32 | 33 | function getDirectiveInfoFromField( 34 | field: FieldNode, 35 | variables: Object, 36 | ): DirectiveInfo { 37 | if (field.directives && field.directives.length) { 38 | const directiveObj: DirectiveInfo = {}; 39 | field.directives.forEach((directive: DirectiveNode) => { 40 | directiveObj[directive.name.value] = argumentsObjectFromField( 41 | directive, 42 | variables, 43 | ); 44 | }); 45 | return directiveObj; 46 | } 47 | return null; 48 | } 49 | 50 | type ResultMapper = ( 51 | values: { [fieldName: string]: any }, 52 | rootValue: any, 53 | ) => any; 54 | 55 | type FragmentMatcher = ( 56 | rootValue: any, 57 | typeCondition: string, 58 | context: any, 59 | ) => boolean; 60 | 61 | export type ExecContext = { 62 | fragmentMap: FragmentMap; 63 | contextValue: any; 64 | variableValues: VariableMap; 65 | resultMapper: ResultMapper; 66 | resolver: Resolver; 67 | fragmentMatcher: FragmentMatcher; 68 | }; 69 | 70 | type ExecOptions = { 71 | resultMapper?: ResultMapper; 72 | fragmentMatcher?: FragmentMatcher; 73 | }; 74 | 75 | const hasOwn = Object.prototype.hasOwnProperty; 76 | 77 | function merge(dest, src) { 78 | if (src !== null && typeof src === 'object') { 79 | Object.keys(src).forEach(key => { 80 | const srcVal = src[key]; 81 | if (!hasOwn.call(dest, key)) { 82 | dest[key] = srcVal; 83 | } else { 84 | merge(dest[key], srcVal); 85 | } 86 | }); 87 | } 88 | } 89 | 90 | type VariableMap = { [name: string]: any }; 91 | 92 | /* Based on graphql function from graphql-js: 93 | * 94 | * graphql( 95 | * schema: GraphQLSchema, 96 | * requestString: string, 97 | * rootValue?: ?any, 98 | * contextValue?: ?any, 99 | * variableValues?: ?{[key: string]: any}, 100 | * operationName?: ?string 101 | * ): Promise 102 | * 103 | */ 104 | export function graphql( 105 | resolver: Resolver, 106 | document: DocumentNode, 107 | rootValue?: any, 108 | contextValue?: any, 109 | variableValues?: VariableMap, 110 | execOptions: ExecOptions = {}, 111 | ): Promise { 112 | const mainDefinition = getMainDefinition(document); 113 | 114 | const fragments = getFragmentDefinitions(document); 115 | const fragmentMap = createFragmentMap(fragments); 116 | 117 | const resultMapper = execOptions.resultMapper; 118 | 119 | // Default matcher always matches all fragments 120 | const fragmentMatcher = execOptions.fragmentMatcher || (() => true); 121 | 122 | const execContext: ExecContext = { 123 | fragmentMap, 124 | contextValue, 125 | variableValues, 126 | resultMapper, 127 | resolver, 128 | fragmentMatcher, 129 | }; 130 | 131 | return executeSelectionSet( 132 | mainDefinition.selectionSet as SelectionSetNode, 133 | rootValue, 134 | execContext, 135 | ); 136 | } 137 | 138 | async function executeSelectionSet( 139 | selectionSet: SelectionSetNode, 140 | rootValue: any, 141 | execContext: ExecContext, 142 | ) { 143 | const { fragmentMap, contextValue, variableValues: variables } = execContext; 144 | 145 | const result = {}; 146 | 147 | const execute = async selection => { 148 | if (!shouldInclude(selection, variables)) { 149 | // Skip this entirely 150 | return; 151 | } 152 | 153 | if (isField(selection)) { 154 | const fieldResult = await executeField( 155 | selection as FieldNode, 156 | rootValue, 157 | execContext, 158 | ); 159 | 160 | const resultFieldKey = resultKeyNameFromField(selection); 161 | 162 | if (fieldResult !== undefined) { 163 | if (result[resultFieldKey] === undefined) { 164 | result[resultFieldKey] = fieldResult; 165 | } else { 166 | merge(result[resultFieldKey], fieldResult); 167 | } 168 | } 169 | 170 | return; 171 | } 172 | 173 | let fragment: InlineFragmentNode | FragmentDefinitionNode; 174 | 175 | if (isInlineFragment(selection)) { 176 | fragment = selection as InlineFragmentNode; 177 | } else { 178 | // This is a named fragment 179 | fragment = fragmentMap[selection.name.value] as FragmentDefinitionNode; 180 | 181 | if (!fragment) { 182 | throw new Error(`No fragment named ${selection.name.value}`); 183 | } 184 | } 185 | 186 | const typeCondition = fragment.typeCondition.name.value; 187 | 188 | if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) { 189 | const fragmentResult = await executeSelectionSet( 190 | fragment.selectionSet, 191 | rootValue, 192 | execContext, 193 | ); 194 | 195 | merge(result, fragmentResult); 196 | } 197 | }; 198 | 199 | await Promise.all(selectionSet.selections.map(execute)); 200 | 201 | if (execContext.resultMapper) { 202 | return execContext.resultMapper(result, rootValue); 203 | } 204 | 205 | return result; 206 | } 207 | 208 | async function executeField( 209 | field: FieldNode, 210 | rootValue: any, 211 | execContext: ExecContext, 212 | ): Promise { 213 | const { variableValues: variables, contextValue, resolver } = execContext; 214 | 215 | const fieldName = field.name.value; 216 | const args = argumentsObjectFromField(field, variables); 217 | 218 | const info: ExecInfo = { 219 | isLeaf: !field.selectionSet, 220 | resultKey: resultKeyNameFromField(field), 221 | directives: getDirectiveInfoFromField(field, variables), 222 | field, 223 | }; 224 | 225 | const result = await resolver(fieldName, rootValue, args, contextValue, info); 226 | 227 | // Handle all scalar types here 228 | if (!field.selectionSet) { 229 | return result; 230 | } 231 | 232 | // From here down, the field has a selection set, which means it's trying to 233 | // query a GraphQLObjectType 234 | if (result == null) { 235 | // Basically any field in a GraphQL response can be null, or missing 236 | return result; 237 | } 238 | 239 | if (Array.isArray(result)) { 240 | return executeSubSelectedArray(field, result, execContext); 241 | } 242 | 243 | // Returned value is an object, and the query has a sub-selection. Recurse. 244 | return executeSelectionSet(field.selectionSet, result, execContext); 245 | } 246 | 247 | function executeSubSelectedArray(field, result, execContext) { 248 | return Promise.all( 249 | result.map(item => { 250 | // null value in array 251 | if (item === null) { 252 | return null; 253 | } 254 | 255 | // This is a nested array, recurse 256 | if (Array.isArray(item)) { 257 | return executeSubSelectedArray(field, item, execContext); 258 | } 259 | 260 | // This is an object, run the selection set on it 261 | return executeSelectionSet(field.selectionSet, item, execContext); 262 | }), 263 | ); 264 | } 265 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es6", "dom"], 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "removeComments": false, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "noImplicitAny": false, 11 | "noUnusedParameters": false, 12 | "noUnusedLocals": true, 13 | "skipLibCheck": true, 14 | "rootDir": "src", 15 | "outDir": "lib" 16 | }, 17 | "include": ["src/**/*.ts"], 18 | "exclude": ["src/**/__tests__/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["src/**/__tests__/*.ts"], 4 | "exclude": [] 5 | } 6 | --------------------------------------------------------------------------------