diff --git a/.changeset/ninety-moles-pay.md b/.changeset/ninety-moles-pay.md new file mode 100644 index 0000000..6b797eb --- /dev/null +++ b/.changeset/ninety-moles-pay.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': major +--- + +Initial release diff --git a/.prettierignore b/.prettierignore index 25552ab..0e6da06 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,5 @@ *.md dist/ + +.volta \ No newline at end of file diff --git a/README.md b/README.md index c4cb6fc..dfb9b4b 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,201 @@ -# TypeScript Template - -This repo is meant to act as a reasonable foundation for a single NPM package developed in TypeScript. It makes use of a few tools that we've found particularly useful. Below are some notes on how to get started using these tools and ways in which you might want (or need) to configure them. - -## GitHub - -In the GitHub settings tab, we typically configure a few things. - -**General** -✅ Automatically delete head branches - -**Collaborators and Teams** -Add relevant teams and contributors with appropriate roles - -**Branches** -Typically we add a branch protection rule for `main` -Generally, this rule enables: -* ✅ Require a pull request before merging - * ✅ Require approvals (1) -* ✅ Require status checks to pass before merging - * Each status check must be selected via the search box. Typing "ci/" will show you a list of the ones which exist within this template. "CLA" should also be enabled. - -**Code Security and Analysis** -* Enable "Dependabot security updates" to receive security-related PRs from Dependabot - -## CircleCI - -This repo comes with a few Circle jobs already implemented (see [`.circleci/config.yml`](.circleci/config.yml)). Circle will run tests on the versions of Node specified in the matrix and enforce linting via Prettier. - -In order to enable CircleCI on your new repo, visit the [Apollo org's dashboard](https://app.circleci.com/projects/project-dashboard/github/apollographql/) and add your project. If your repo has already been initialized and added to the apollographql org, you should see the option to add your new project. - -## Jest - -Jest is a testing framework used by most of Apollo's current projects. - -To run tests in the repo: -`npm test` - -The Jest configuration can be found at `jest.config.ts`. As configured, Jest will run all files named `*.test.ts` found within any `__tests__` folder. This is simply a convention chosen by this repo and can be reconfigured via the `testRegex` configuration option in [`jest.config.ts`](jest.config.ts). - -For more information on configuring Jest see the [Jest docs](https://jestjs.io/docs/configuration). - -## Changesets - -Changesets is a tool for managing package versioning, NPM releases, GitHub releases, and CHANGELOG entries. In this template, it comes configured for all of the above. - -### Basic usage - -Changesets uses changeset files in the `.changeset` directory to determine what versioning upgrades need to happen next, along with related `CHANGELOG` updates and release notes. A changeset file is created by running `npx changeset` and following the prompts. PRs which make functional changes to the package should always come with an accompanying changeset file. The Changeset bot (details below) will comment on PRs as a reminder to contributors to include a changeset file when appropriate. - -### Changeset bot - -#### Installation - -[GitHub app](https://github.com/apps/changeset-bot) -> Note: a GitHub _org_ admin must approve app installations. By adding a GitHub app to your repo, you'll be submitting a request for approval. At the time of writing this, the GitHub UI doesn't make this clear. - -You might also be interested in adding `changeset-bot` to the repo - it leaves comments about the changeset (or lack thereof) for each PR. This serves as a nice reminder and set of instructions for how to create a changeset. -### CHANGELOG updates - -For proper CHANGELOG management, you MUST configure the [`.changeset/config.json`](.changeset/config.json) file for your repo. The `changelog.repo` field must be the `/` of the repo. - -### NPM Publishing - -Changesets manages and updates a release PR automatically via a GitHub action [`.github/workflows/release-pr.yml`](.github/workflows/release-pr.yml). The PR consumes all of the committed changesets on `main` in order to bump versions of packages and update the `CHANGELOG` accordingly. Merging this PR will result in publishes to npm IF you've provided an `NPM_TOKEN` as a secret to your repo's GitHub actions (`https://github.com/apollographql//settings/secrets/actions`). Please reach out to anyone in the `#npm-apollo-bot-owners` Slack channel for a token. Changesets will also publish a GitHub release when this PR is merged. - -> Our action borrows directly from the action provided by `changesets`. Visit [the changesets action repo](https://github.com/changesets/action) for more info. - -### Removing Changesets - -If you're not interested in using `changesets`, just delete the [workflow](.github/workflows/release-pr.yml), uninstall the related dependencies, and delete the related scripts. - -> For additional information on `changesets`, [visit the docs](https://github.com/changesets/changesets#documentation). - -## CodeSandbox CI - -> At the time of writing this, CodeSandbox CI only works for public repos. - -### Installation - -[GitHub app](https://github.com/apps/codesandbox) -> Note: a GitHub _org_ admin must approve app installations. By adding a GitHub app to your repo, you'll be submitting a request for approval. At the time of writing this, the GitHub UI doesn't make this clear. - -CodeSandbox CI provides an installable build of your package on every PR. If your package builds successfully, CS:CI will leave a comment on the PR with instructions on how to try out your build in a project. This gives contributors access to their work immediately, allowing them to manually test their builds or even use a fix right away. - -CS:CI will also provide links to sandboxes which use your newly built package if you choose. This is configurable via the `sandboxes` field in [`.codesandbox/ci.json`](.codesandbox/ci.json). This field is a list of sandbox IDs which you can find via the CodeSandbox web interface. For example, the Apollo Server repo specifies both JS and TS Apollo Server sandboxes like so: `["apollo-server-typescript-3opde","apollo-server"]`. - -> For additional information on configuring CS:CI, [visit the docs](https://codesandbox.io/docs/ci). - -## Renovate - -### Installation - -[GitHub app](https://github.com/apps/renovate) - -> Note: a GitHub _org_ admin must approve app installations. By adding a GitHub app to your repo, you'll be submitting a request for approval. At the time of writing this, the GitHub UI doesn't make this clear. - -Renovate automates dependency updates. The bot will open and merge PRs with updates to a variety of dependencies (including but not limited to npm dependencies). Renovate is _highly_ configurable via the [renovate.json5](renovate.json5) file. Package restrictions and scheduling are just a couple things that we commonly configure. - -If you've configured PRs to require approval (mentioned in [GitHub](#github)), you may want to also install [Renovate's Approve bot](https://github.com/apps/renovate-approve). The approve bot will approve all renovate PRs in order to appease the PR approval requirement. - -If you're unfamiliar with Renovate, the docs are really worth perusing even if just to get an idea of what kinds of configuration are possible. - -> For additional information on configuring Renovate, [visit the docs](https://docs.renovatebot.com/). - -## Prettier - -Prettier is an opinionated code formatting tool. - -To check for formatting issues: -`npm run prettier:check` - -To auto-fix formatting issues: -`npm run prettier:fix` - -This is enforced in CI via the `Prettier` job. - -> For additional information on configuring Prettier, [visit the docs](https://prettier.io/docs/en/options). - -## Volta - -Volta is a fast JS toolchain manager. Similar in effect to nvm, Volta allows for projects to specify their node / npm versions and will automatically handle the switching for you. - -If using [direnv](https://direnv.net/), Volta will automatically be installed for you. The node and npm versions are specified in [`package.json`](package.json) and Renovate handles keeping these versions up to date. - -> For additional information on configuring Volta, [visit the docs](https://docs.volta.sh/guide/). \ No newline at end of file +# Apollo REST Data Source + +This package exports a ([`RESTDataSource`](https://github.com/apollographql/datasource-rest#apollo-rest-data-source)) class which is used for fetching data from a REST API and exposing it via GraphQL within Apollo Server. + +## Documentation + +View the [Apollo Server documentation for data sources](https://www.apollographql.com/docs/apollo-server/features/data-sources/) for more details. + +## Usage + +To get started, install the `@apollo/datasource-rest` package: + +```bash +npm install @apollo/datasource-rest +``` + +To define a data source, extend the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) class and implement the data fetching methods that your resolvers require. Data sources can then be provided via Apollo Server's `context` object during execution. + +Your implementation of these methods can call on convenience methods built into the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) class to perform HTTP requests, while making it easy to build up query parameters, parse JSON results, and handle errors. + +```javascript +const { RESTDataSource } = require('@apollo/datasource-rest'); + +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'https://movies-api.example.com/'; + } + + async getMovie(id) { + return this.get(`movies/${encodeURIComponent(id)}`); + } + + async getMostViewedMovies(limit = 10) { + const data = await this.get('movies', { + params: { + per_page: limit, + order_by: 'most_viewed', + }, + }); + return data.results; + } +} +``` + +### HTTP Methods + +The `get` method on the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) makes an HTTP `GET` request. Similarly, there are methods built-in to allow for `POST`, `PUT`, `PATCH`, and `DELETE` requests. + +```javascript +class MoviesAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = 'https://movies-api.example.com/'; + } + + // an example making an HTTP POST request + async postMovie(movie) { + return this.post( + `movies`, // path + { body: movie }, // request body + ); + } + + // an example making an HTTP PUT request + async newMovie(movie) { + return this.put( + `movies`, // path + { body: movie }, // request body + ); + } + + // an example making an HTTP PATCH request + async updateMovie(movie) { + return this.patch( + `movies`, // path + { body: { id: movie.id, movie } }, // request body + ); + } + + // an example making an HTTP DELETE request + async deleteMovie(movie) { + return this.delete( + `movies/${encodeURIComponent(movie.id)}`, // path + ); + } +} +``` + +All of the HTTP helper functions (`get`, `put`, `post`, `patch`, and `delete`) accept a second parameter for setting the `body`, `headers`, `params`, and `cacheOptions`. + +### Intercepting fetches + +Data sources allow you to intercept fetches to set headers, query parameters, or make other changes to the outgoing request. This is most often used for authorization or other common concerns that apply to all requests. Data sources also get access to the GraphQL context, which is a great place to store a user token or other information you need to have available. + +You can easily set a header on every request: + +```javascript +class PersonalizationAPI extends RESTDataSource { + willSendRequest(request) { + request.headers['authorization'] = this.context.token; + } +} +``` + +Or add a query parameter: + +```javascript +class PersonalizationAPI extends RESTDataSource { + willSendRequest(request) { + request.params.set('api_key', this.context.token); + } +} +``` + +If you're using TypeScript, you can use the `RequestOptions` type to define the `willSendRequest` signature: +```ts +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; + +class PersonalizationAPI extends RESTDataSource { + override baseURL = 'https://personalization-api.example.com/'; + + constructor(private token: string) { + super(); + } + + override willSendRequest(request: WillSendRequestOptions) { + request.headers['authorization'] = this.token; + } +} +``` + +### Resolving URLs dynamically + +In some cases, you'll want to set the URL based on the environment or other contextual values. To do this, you can override `resolveURL`: + +```ts +class PersonalizationAPI extends RESTDataSource { + constructor(private token: string) { + super(); + } + + override async resolveURL(path: string) { + if (!this.baseURL) { + const addresses = await resolveSrv(path.split("/")[1] + ".service.consul"); + this.baseURL = addresses[0]; + } + return super.resolveURL(path); + } +} +``` + +### Accessing data sources from resolvers + +To give resolvers access to data sources, you pass them as options to the `ApolloServer` constructor: + +```ts +interface MyContext { + movies: MoviesAPI; + personalization: PersonalizationAPI; +} + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +// The context function you provide to your integration should handle constructing your data sources on every request. +const url = await startStandaloneServer(server, { + async context({ req }) { + return { + moviesAPI: new MoviesAPI(), + personalizationAPI: new PersonalizationAPI(req.headers['authorization']), + }; + }, +}); +``` + +From our resolvers, we can access the data source from context and return the result: + +```javascript +const resolvers = { + Query: { + movie: async (_source, { id }, { moviesAPI }) => { + return moviesAPI.getMovie(id); + }, + mostViewedMovies: async (_source, _args, { moviesAPI }) => { + return moviesAPI.getMostViewedMovies(); + }, + favorites: async (_source, _args, { personalizationAPI }) => { + return personalizationAPI.getFavorites(); + }, + }, +}; +``` + +### Implementing custom metrics + +By overriding `trace` method, it's possible to implement custom metrics for request timing. + +See the original method [implementation](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) or the reference. diff --git a/cspell-dict.txt b/cspell-dict.txt index 3f21f0a..4d705d8 100644 --- a/cspell-dict.txt +++ b/cspell-dict.txt @@ -4,4 +4,5 @@ cimg circleci codesandbox direnv +httpcache opde diff --git a/package-lock.json b/package-lock.json index a8db331..f804213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,25 @@ "version": "3.0.0", "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@apollo/utils.fetcher": "^1.0.0", + "@apollo/utils.keyvaluecache": "1.0.1", + "http-cache-semantics": "^4.1.0", + "node-fetch": "^2.6.7" + }, "devDependencies": { + "@apollo/server": "4.0.0-alpha.2", + "@apollo/utils.withrequired": "^1.0.0", "@changesets/changelog-github": "0.4.6", "@changesets/cli": "2.24.1", + "@types/http-cache-semantics": "4.0.1", "@types/jest": "28.1.6", "@types/node": "14.18.23", "cspell": "6.5.0", + "graphql": "16.5.0", "jest": "28.1.3", "jest-junit": "14.0.0", + "nock": "13.2.9", "prettier": "2.7.1", "ts-jest": "28.0.7", "ts-node": "10.9.1", @@ -24,6 +35,9 @@ }, "engines": { "node": ">=14.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" } }, "node_modules/@ampproject/remapping": { @@ -38,6 +52,241 @@ "node": ">=6.0.0" } }, + "node_modules/@apollo/protobufjs": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.4.tgz", + "integrity": "sha512-npVJ9NVU/pynj+SCU+fambvTneJDyCnif738DnZ7pCxdDtzeEz7WkpSIq5wNUmWm5Td55N+S2xfqZ+WP4hDLng==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "bin": { + "apollo-pbjs": "bin/pbjs", + "apollo-pbts": "bin/pbts" + } + }, + "node_modules/@apollo/protobufjs/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, + "node_modules/@apollo/server": { + "version": "4.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.0.0-alpha.2.tgz", + "integrity": "sha512-6bq9TjsXLRIAQJg2S1GHGoihOz24XPtd6JbOZuhUHvpujfY5vZzNJHvfGgVXYvg0aiz6ywUA/GZuDipbW874iA==", + "dev": true, + "dependencies": { + "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", + "@apollo/utils.createhash": "^1.1.0", + "@apollo/utils.fetcher": "^1.0.0", + "@apollo/utils.isnodelike": "^1.1.0", + "@apollo/utils.keyvaluecache": "^1.0.1", + "@apollo/utils.logger": "^1.0.0", + "@apollo/utils.usagereporting": "^1.0.0", + "@apollo/utils.withrequired": "^1.0.0", + "@apollographql/graphql-playground-html": "1.6.29", + "@graphql-tools/schema": "^8.0.0", + "@josephg/resolvable": "^1.0.0", + "@types/express": "4.17.13", + "@types/express-serve-static-core": "4.17.30", + "@types/node-fetch": "^2.6.1", + "async-retry": "^1.2.1", + "body-parser": "^1.20.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "loglevel": "^1.6.8", + "lru-cache": "^7.10.1", + "negotiator": "^0.6.3", + "node-fetch": "^2.6.7", + "uuid": "^8.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "graphql": "^16.5.0" + } + }, + "node_modules/@apollo/server/node_modules/lru-cache": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", + "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@apollo/usage-reporting-protobuf": { + "version": "4.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.0.0-alpha.1.tgz", + "integrity": "sha512-5pN8CQGxvGbpm58VK7EguR5d6rwZ+v2B9MddKM4DWnh87DuUEbu2xzcSBGaj2Yk2kVzro9YJ1J0vrQrQn8ESYQ==", + "dev": true, + "dependencies": { + "@apollo/protobufjs": "1.2.4" + } + }, + "node_modules/@apollo/utils.createhash": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-1.1.0.tgz", + "integrity": "sha512-5fT4ZiW75515OlikWpIQzaVDws1yy9VgYSoHoJCrvI2UH6/7YNKXQjbjT5qVYu6ytch2wBxFMfFfYWMn/2bSCQ==", + "dev": true, + "dependencies": { + "@apollo/utils.isnodelike": "^1.1.0", + "sha.js": "^2.4.11" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@apollo/utils.dropunuseddefinitions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-1.1.0.tgz", + "integrity": "sha512-jU1XjMr6ec9pPoL+BFWzEPW7VHHulVdGKMkPAMiCigpVIT11VmCbnij0bWob8uS3ODJ65tZLYKAh/55vLw2rbg==", + "dev": true, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.fetcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-1.0.0.tgz", + "integrity": "sha512-SpJH69ffk91BoYSVb12Dt/jFQKVOrm4NE59XUeHXRsha1xBmxjvZNN6qvYySAcGjloW4TtFZYlPkBpwjMRWymw==" + }, + "node_modules/@apollo/utils.isnodelike": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-1.1.0.tgz", + "integrity": "sha512-q/Q82kBUSEcx1ED11JO1TYBY781mWluUnBD8NvhjHVsu1K1C5R9BZVUxShyK/V8XcePcRUB5fdWOcBMGwS0KOA==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@apollo/utils.keyvaluecache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", + "integrity": "sha512-nLgYLomqjVimEzQ4cdvVQkcryi970NDvcRVPfd0OPeXhBfda38WjBq+WhQFk+czSHrmrSp34YHBxpat0EtiowA==", + "dependencies": { + "@apollo/utils.logger": "^1.0.0", + "lru-cache": "^7.10.1" + } + }, + "node_modules/@apollo/utils.keyvaluecache/node_modules/lru-cache": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", + "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@apollo/utils.logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", + "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==" + }, + "node_modules/@apollo/utils.printwithreducedwhitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-1.1.0.tgz", + "integrity": "sha512-GfFSkAv3n1toDZ4V6u2d7L4xMwLA+lv+6hqXicMN9KELSJ9yy9RzuEXaX73c/Ry+GzRsBy/fdSUGayGqdHfT2Q==", + "dev": true, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.removealiases": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-1.0.0.tgz", + "integrity": "sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==", + "dev": true, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.sortast": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-1.1.0.tgz", + "integrity": "sha512-VPlTsmUnOwzPK5yGZENN069y6uUHgeiSlpEhRnLFYwYNoJHsuJq2vXVwIaSmts015WTPa2fpz1inkLYByeuRQA==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.stripsensitiveliterals": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-1.2.0.tgz", + "integrity": "sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==", + "dev": true, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.usagereporting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-1.0.0.tgz", + "integrity": "sha512-5PL7hJMkTPmdo3oxPtigRrIyPxDk/ddrUryHPDaezL1lSFExpNzsDd2f1j0XJoHOg350GRd3LyD64caLA2PU1w==", + "dev": true, + "dependencies": { + "@apollo/utils.dropunuseddefinitions": "^1.1.0", + "@apollo/utils.printwithreducedwhitespace": "^1.1.0", + "@apollo/utils.removealiases": "1.0.0", + "@apollo/utils.sortast": "^1.1.0", + "@apollo/utils.stripsensitiveliterals": "^1.2.0", + "apollo-reporting-protobuf": "^3.3.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.withrequired": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-1.0.0.tgz", + "integrity": "sha512-wmsGhLTMzaEYsjjSXwP3rdsmwsEONs1Cw5FbSUplRYY53mnYexCV/5xsFhcZFn9yFjARQE0uS8glsPvrDxgabA==", + "dev": true + }, + "node_modules/@apollographql/graphql-playground-html": { + "version": "1.6.29", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", + "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "dev": true, + "dependencies": { + "xss": "^1.0.8" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -1252,6 +1501,46 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@graphql-tools/merge": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz", + "integrity": "sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz", + "integrity": "sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==", + "dev": true, + "dependencies": { + "@graphql-tools/merge": "8.3.1", + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz", + "integrity": "sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1919,6 +2208,12 @@ "node": ">=8" } }, + "node_modules/@josephg/resolvable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", + "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==", + "dev": true + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", @@ -2039,6 +2334,70 @@ "node": ">= 8" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.24.19", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz", @@ -2128,6 +2487,48 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.30", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2137,6 +2538,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, "node_modules/@types/is-ci": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz", @@ -2180,6 +2587,18 @@ "pretty-format": "^28.0.0" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -2220,12 +2639,34 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, "node_modules/@types/semver": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz", "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==", "dev": true }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -2247,6 +2688,19 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -2338,6 +2792,15 @@ "node": ">= 8" } }, + "node_modules/apollo-reporting-protobuf": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.3.2.tgz", + "integrity": "sha512-j1tx9tmkVdsLt1UPzBrvz90PdjAeKW157WxGn+aXlnnGfVjZLIRXX3x5t1NWtXvB7rVaAsLLILLtDHW382TSoQ==", + "dev": true, + "dependencies": { + "@apollo/protobufjs": "1.2.4" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2353,6 +2816,12 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -2395,6 +2864,15 @@ "node": ">=0.10.0" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2580,6 +3058,45 @@ "node": ">=4" } }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2661,6 +3178,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2887,6 +3413,47 @@ "node": ">=8" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -2896,12 +3463,40 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -3447,6 +4042,12 @@ "node": ">= 10.0.0" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "dev": true + }, "node_modules/csv": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.3.tgz", @@ -3583,6 +4184,25 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -3652,6 +4272,12 @@ "node": ">=10" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.73", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.73.tgz", @@ -3676,6 +4302,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -3769,6 +4404,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3791,6 +4432,15 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3889,6 +4539,83 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -3979,6 +4706,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4035,6 +4795,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4277,6 +5055,15 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphql": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", + "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -4376,6 +5163,27 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-id": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/human-id/-/human-id-1.0.2.tgz", @@ -4522,6 +5330,15 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6576,6 +7393,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -6657,18 +7480,49 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "node_modules/lodash.startcase": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", "integrity": "sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg=", "dev": true }, + "node_modules/loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6732,6 +7586,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/meow": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", @@ -6757,6 +7620,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6772,6 +7641,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -6785,6 +7663,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6883,11 +7773,34 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -6948,6 +7861,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -6984,6 +7906,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7110,6 +8044,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7143,6 +8086,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7331,12 +8280,49 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7366,6 +8352,30 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7556,6 +8566,15 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7625,12 +8644,91 @@ "semver": "bin/semver" } }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -7881,6 +8979,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stream-transform": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-2.1.3.tgz", @@ -8138,11 +9245,19 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "node_modules/trim-newlines": { "version": "3.0.1", @@ -8263,6 +9378,12 @@ } } }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, "node_modules/tty-table": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", @@ -8384,6 +9505,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -8442,6 +9576,24 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -8481,6 +9633,24 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", @@ -8514,14 +9684,21 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -8657,6 +9834,28 @@ "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", "dev": true }, + "node_modules/xss": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz", + "integrity": "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==", + "dev": true, + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8753,6 +9952,192 @@ "@jridgewell/trace-mapping": "^0.3.0" } }, + "@apollo/protobufjs": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.4.tgz", + "integrity": "sha512-npVJ9NVU/pynj+SCU+fambvTneJDyCnif738DnZ7pCxdDtzeEz7WkpSIq5wNUmWm5Td55N+S2xfqZ+WP4hDLng==", + "dev": true, + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + } + } + }, + "@apollo/server": { + "version": "4.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.0.0-alpha.2.tgz", + "integrity": "sha512-6bq9TjsXLRIAQJg2S1GHGoihOz24XPtd6JbOZuhUHvpujfY5vZzNJHvfGgVXYvg0aiz6ywUA/GZuDipbW874iA==", + "dev": true, + "requires": { + "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", + "@apollo/utils.createhash": "^1.1.0", + "@apollo/utils.fetcher": "^1.0.0", + "@apollo/utils.isnodelike": "^1.1.0", + "@apollo/utils.keyvaluecache": "^1.0.1", + "@apollo/utils.logger": "^1.0.0", + "@apollo/utils.usagereporting": "^1.0.0", + "@apollo/utils.withrequired": "^1.0.0", + "@apollographql/graphql-playground-html": "1.6.29", + "@graphql-tools/schema": "^8.0.0", + "@josephg/resolvable": "^1.0.0", + "@types/express": "4.17.13", + "@types/express-serve-static-core": "4.17.30", + "@types/node-fetch": "^2.6.1", + "async-retry": "^1.2.1", + "body-parser": "^1.20.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "loglevel": "^1.6.8", + "lru-cache": "^7.10.1", + "negotiator": "^0.6.3", + "node-fetch": "^2.6.7", + "uuid": "^8.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", + "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", + "dev": true + } + } + }, + "@apollo/usage-reporting-protobuf": { + "version": "4.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.0.0-alpha.1.tgz", + "integrity": "sha512-5pN8CQGxvGbpm58VK7EguR5d6rwZ+v2B9MddKM4DWnh87DuUEbu2xzcSBGaj2Yk2kVzro9YJ1J0vrQrQn8ESYQ==", + "dev": true, + "requires": { + "@apollo/protobufjs": "1.2.4" + } + }, + "@apollo/utils.createhash": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-1.1.0.tgz", + "integrity": "sha512-5fT4ZiW75515OlikWpIQzaVDws1yy9VgYSoHoJCrvI2UH6/7YNKXQjbjT5qVYu6ytch2wBxFMfFfYWMn/2bSCQ==", + "dev": true, + "requires": { + "@apollo/utils.isnodelike": "^1.1.0", + "sha.js": "^2.4.11" + } + }, + "@apollo/utils.dropunuseddefinitions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-1.1.0.tgz", + "integrity": "sha512-jU1XjMr6ec9pPoL+BFWzEPW7VHHulVdGKMkPAMiCigpVIT11VmCbnij0bWob8uS3ODJ65tZLYKAh/55vLw2rbg==", + "dev": true, + "requires": {} + }, + "@apollo/utils.fetcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-1.0.0.tgz", + "integrity": "sha512-SpJH69ffk91BoYSVb12Dt/jFQKVOrm4NE59XUeHXRsha1xBmxjvZNN6qvYySAcGjloW4TtFZYlPkBpwjMRWymw==" + }, + "@apollo/utils.isnodelike": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-1.1.0.tgz", + "integrity": "sha512-q/Q82kBUSEcx1ED11JO1TYBY781mWluUnBD8NvhjHVsu1K1C5R9BZVUxShyK/V8XcePcRUB5fdWOcBMGwS0KOA==", + "dev": true + }, + "@apollo/utils.keyvaluecache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", + "integrity": "sha512-nLgYLomqjVimEzQ4cdvVQkcryi970NDvcRVPfd0OPeXhBfda38WjBq+WhQFk+czSHrmrSp34YHBxpat0EtiowA==", + "requires": { + "@apollo/utils.logger": "^1.0.0", + "lru-cache": "^7.10.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", + "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==" + } + } + }, + "@apollo/utils.logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", + "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==" + }, + "@apollo/utils.printwithreducedwhitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-1.1.0.tgz", + "integrity": "sha512-GfFSkAv3n1toDZ4V6u2d7L4xMwLA+lv+6hqXicMN9KELSJ9yy9RzuEXaX73c/Ry+GzRsBy/fdSUGayGqdHfT2Q==", + "dev": true, + "requires": {} + }, + "@apollo/utils.removealiases": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-1.0.0.tgz", + "integrity": "sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==", + "dev": true, + "requires": {} + }, + "@apollo/utils.sortast": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-1.1.0.tgz", + "integrity": "sha512-VPlTsmUnOwzPK5yGZENN069y6uUHgeiSlpEhRnLFYwYNoJHsuJq2vXVwIaSmts015WTPa2fpz1inkLYByeuRQA==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0" + } + }, + "@apollo/utils.stripsensitiveliterals": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-1.2.0.tgz", + "integrity": "sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==", + "dev": true, + "requires": {} + }, + "@apollo/utils.usagereporting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-1.0.0.tgz", + "integrity": "sha512-5PL7hJMkTPmdo3oxPtigRrIyPxDk/ddrUryHPDaezL1lSFExpNzsDd2f1j0XJoHOg350GRd3LyD64caLA2PU1w==", + "dev": true, + "requires": { + "@apollo/utils.dropunuseddefinitions": "^1.1.0", + "@apollo/utils.printwithreducedwhitespace": "^1.1.0", + "@apollo/utils.removealiases": "1.0.0", + "@apollo/utils.sortast": "^1.1.0", + "@apollo/utils.stripsensitiveliterals": "^1.2.0", + "apollo-reporting-protobuf": "^3.3.1" + } + }, + "@apollo/utils.withrequired": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-1.0.0.tgz", + "integrity": "sha512-wmsGhLTMzaEYsjjSXwP3rdsmwsEONs1Cw5FbSUplRYY53mnYexCV/5xsFhcZFn9yFjARQE0uS8glsPvrDxgabA==", + "dev": true + }, + "@apollographql/graphql-playground-html": { + "version": "1.6.29", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", + "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "dev": true, + "requires": { + "xss": "^1.0.8" + } + }, "@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -9838,6 +11223,37 @@ } } }, + "@graphql-tools/merge": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz", + "integrity": "sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==", + "dev": true, + "requires": { + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz", + "integrity": "sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==", + "dev": true, + "requires": { + "@graphql-tools/merge": "8.3.1", + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/utils": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz", + "integrity": "sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10343,6 +11759,12 @@ } } }, + "@josephg/resolvable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", + "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==", + "dev": true + }, "@jridgewell/resolve-uri": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", @@ -10449,6 +11871,70 @@ "fastq": "^1.6.0" } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true + }, "@sinclair/typebox": { "version": "0.24.19", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz", @@ -10538,6 +12024,48 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.30", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -10547,6 +12075,12 @@ "@types/node": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, "@types/is-ci": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz", @@ -10590,6 +12124,18 @@ "pretty-format": "^28.0.0" } }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "dev": true + }, + "@types/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==", + "dev": true + }, "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -10630,12 +12176,34 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, "@types/semver": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz", "integrity": "sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==", "dev": true }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -10657,6 +12225,16 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", @@ -10717,6 +12295,15 @@ "picomatch": "^2.0.4" } }, + "apollo-reporting-protobuf": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.3.2.tgz", + "integrity": "sha512-j1tx9tmkVdsLt1UPzBrvz90PdjAeKW157WxGn+aXlnnGfVjZLIRXX3x5t1NWtXvB7rVaAsLLILLtDHW382TSoQ==", + "dev": true, + "requires": { + "@apollo/protobufjs": "1.2.4" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -10732,6 +12319,12 @@ "sprintf-js": "~1.0.2" } }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -10762,6 +12355,15 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "requires": { + "retry": "0.13.1" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10904,6 +12506,43 @@ "is-windows": "^1.0.0" } }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -10969,6 +12608,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -11145,6 +12790,29 @@ "xdg-basedir": "^4.0.0" } }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -11154,12 +12822,34 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -11568,6 +13258,12 @@ } } }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "dev": true + }, "csv": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.3.tgz", @@ -11674,6 +13370,18 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, "detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -11722,6 +13430,12 @@ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", "dev": true }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "electron-to-chromium": { "version": "1.4.73", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.73.tgz", @@ -11740,6 +13454,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -11815,6 +13535,12 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -11827,6 +13553,12 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -11900,6 +13632,68 @@ "jest-util": "^28.1.3" } }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -11978,6 +13772,38 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -12025,6 +13851,18 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, "fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -12194,6 +14032,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphql": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", + "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==", + "dev": true + }, "hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -12263,6 +14107,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "human-id": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/human-id/-/human-id-1.0.2.tgz", @@ -12372,6 +14234,12 @@ "side-channel": "^1.0.4" } }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -13873,6 +15741,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -13933,18 +15807,42 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "lodash.startcase": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", "integrity": "sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg=", "dev": true }, + "loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==", + "dev": true + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -13992,6 +15890,12 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, "meow": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", @@ -14011,6 +15915,12 @@ "yargs-parser": "^18.1.3" } }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -14023,6 +15933,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -14033,6 +15949,12 @@ "picomatch": "^2.3.1" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -14104,11 +16026,28 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + } + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, "requires": { "whatwg-url": "^5.0.0" } @@ -14152,6 +16091,12 @@ "path-key": "^3.0.0" } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -14176,6 +16121,15 @@ "object-keys": "^1.1.1" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -14266,6 +16220,12 @@ "lines-and-columns": "^1.1.6" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14290,6 +16250,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -14416,12 +16382,37 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14434,6 +16425,24 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -14577,6 +16586,12 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -14619,12 +16634,86 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -14837,6 +16926,12 @@ } } }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, "stream-transform": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-2.1.3.tgz", @@ -15024,11 +17119,16 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "trim-newlines": { "version": "3.0.1", @@ -15090,6 +17190,12 @@ "yn": "3.1.1" } }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, "tty-table": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", @@ -15174,6 +17280,16 @@ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -15216,6 +17332,18 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -15249,6 +17377,18 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, "vscode-languageserver-textdocument": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", @@ -15282,14 +17422,18 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -15400,6 +17544,24 @@ "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", "dev": true }, + "xss": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz", + "integrity": "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==", + "dev": true, + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b61394c..72e5315 100644 --- a/package.json +++ b/package.json @@ -32,18 +32,32 @@ "watch": "tsc --build --watch" }, "devDependencies": { + "@apollo/server": "4.0.0-alpha.2", + "@apollo/utils.withrequired": "^1.0.0", "@changesets/changelog-github": "0.4.6", "@changesets/cli": "2.24.1", + "@types/http-cache-semantics": "4.0.1", "@types/jest": "28.1.6", "@types/node": "14.18.23", "cspell": "6.5.0", + "graphql": "16.5.0", "jest": "28.1.3", "jest-junit": "14.0.0", + "nock": "13.2.9", "prettier": "2.7.1", "ts-jest": "28.0.7", "ts-node": "10.9.1", "typescript": "4.7.4" }, + "dependencies": { + "@apollo/utils.fetcher": "^1.0.0", + "@apollo/utils.keyvaluecache": "1.0.1", + "http-cache-semantics": "^4.1.0", + "node-fetch": "^2.6.7" + }, + "peerDependencies": { + "graphql": "^16.5.0" + }, "volta": { "node": "16.16.0", "npm": "8.15.1" diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index a16e751..825387e 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -1,21 +1,24 @@ -import { fetch, Request, Response, Headers } from 'apollo-server-env'; - -import CachePolicy = require('http-cache-semantics'); - +import fetch, { Response } from 'node-fetch'; +import CachePolicy from 'http-cache-semantics'; +import type { + Fetcher, + FetcherResponse, + FetcherRequestInit, +} from '@apollo/utils.fetcher'; import { - KeyValueCache, InMemoryLRUCache, + KeyValueCache, PrefixingKeyValueCache, } from '@apollo/utils.keyvaluecache'; -import type { CacheOptions } from './RESTDataSource'; +import type { CacheOptions, RequestOptions } from './RESTDataSource'; export class HTTPCache { private keyValueCache: KeyValueCache; - private httpFetch: typeof fetch; + private httpFetch: Fetcher; constructor( keyValueCache: KeyValueCache = new InMemoryLRUCache(), - httpFetch: typeof fetch = fetch, + httpFetch: Fetcher = fetch, ) { this.keyValueCache = new PrefixingKeyValueCache( keyValueCache, @@ -25,31 +28,39 @@ export class HTTPCache { } async fetch( - request: Request, - options: { + url: URL, + requestOpts: FetcherRequestInit = {}, + cache?: { cacheKey?: string; cacheOptions?: | CacheOptions - | ((response: Response, request: Request) => CacheOptions | undefined); - } = {}, - ): Promise { - const cacheKey = options.cacheKey ? options.cacheKey : request.url; + | (( + url: string, + response: FetcherResponse, + request: RequestOptions, + ) => CacheOptions | undefined); + }, + ): Promise { + const urlString = url.toString(); + requestOpts.method = requestOpts.method ?? 'GET'; + const cacheKey = cache?.cacheKey ?? urlString; const entry = await this.keyValueCache.get(cacheKey); if (!entry) { - const response = await this.httpFetch(request); + const response = await this.httpFetch(urlString, requestOpts); const policy = new CachePolicy( - policyRequestFrom(request), + policyRequestFrom(urlString, requestOpts), policyResponseFrom(response), ); return this.storeResponseAndReturnClone( + urlString, response, - request, + requestOpts, policy, cacheKey, - options.cacheOptions, + cache?.cacheOptions, ); } @@ -62,7 +73,9 @@ export class HTTPCache { if ( (ttlOverride && policy.age() < ttlOverride) || (!ttlOverride && - policy.satisfiesWithoutRevalidation(policyRequestFrom(request))) + policy.satisfiesWithoutRevalidation( + policyRequestFrom(urlString, requestOpts), + )) ) { const headers = policy.responseHeaders(); return new Response(body, { @@ -72,43 +85,53 @@ export class HTTPCache { }); } else { const revalidationHeaders = policy.revalidationHeaders( - policyRequestFrom(request), + policyRequestFrom(urlString, requestOpts), ); - const revalidationRequest = new Request(request, { + const revalidationRequest: RequestOptions = { + ...requestOpts, headers: revalidationHeaders, - }); - const revalidationResponse = await this.httpFetch(revalidationRequest); + }; + const revalidationResponse = await this.httpFetch( + urlString, + revalidationRequest, + ); const { policy: revalidatedPolicy, modified } = policy.revalidatedPolicy( - policyRequestFrom(revalidationRequest), + policyRequestFrom(urlString, revalidationRequest), policyResponseFrom(revalidationResponse), ); return this.storeResponseAndReturnClone( + urlString, new Response(modified ? await revalidationResponse.text() : body, { url: revalidatedPolicy._url, status: revalidatedPolicy._status, headers: revalidatedPolicy.responseHeaders(), }), - request, + requestOpts, revalidatedPolicy, cacheKey, - options.cacheOptions, + cache?.cacheOptions, ); } } private async storeResponseAndReturnClone( - response: Response, - request: Request, + url: string, + response: FetcherResponse, + request: RequestOptions, policy: CachePolicy, cacheKey: string, cacheOptions?: | CacheOptions - | ((response: Response, request: Request) => CacheOptions | undefined), - ): Promise { + | (( + url: string, + response: FetcherResponse, + request: RequestOptions, + ) => CacheOptions | undefined), + ): Promise { if (typeof cacheOptions === 'function') { - cacheOptions = cacheOptions(response, request); + cacheOptions = cacheOptions(url, response, request); } let ttlOverride = cacheOptions?.ttl; @@ -153,34 +176,26 @@ export class HTTPCache { url: response.url, status: response.status, statusText: response.statusText, - headers: response.headers, + headers: Object.fromEntries(response.headers), }); } } -function canBeRevalidated(response: Response): boolean { +function canBeRevalidated(response: FetcherResponse): boolean { return response.headers.has('ETag'); } -function policyRequestFrom(request: Request) { +function policyRequestFrom(url: string, request: RequestOptions) { return { - url: request.url, - method: request.method, - headers: headersToObject(request.headers), + url, + method: request.method ?? 'GET', + headers: request.headers ?? {}, }; } -function policyResponseFrom(response: Response) { +function policyResponseFrom(response: FetcherResponse) { return { status: response.status, - headers: headersToObject(response.headers), + headers: Object.fromEntries(response.headers), }; } - -function headersToObject(headers: Headers) { - const object = Object.create(null); - for (const [name, value] of headers) { - object[name] = value; - } - return object; -} diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index 4d016dc..abeddf5 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -1,62 +1,65 @@ -import { - Request, - RequestInit, - Response, - BodyInit, - Headers, - URL, - URLSearchParams, - URLSearchParamsInit, - fetch, -} from 'apollo-server-env'; - -import { DataSource, DataSourceConfig } from 'apollo-datasource'; - import { HTTPCache } from './HTTPCache'; - -import { - ApolloError, - AuthenticationError, - ForbiddenError, -} from 'apollo-server-errors'; +import { GraphQLError } from 'graphql'; +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; +import type { + Fetcher, + FetcherRequestInit, + FetcherResponse, +} from '@apollo/utils.fetcher'; +import type { WithRequired } from '@apollo/utils.withrequired'; type ValueOrPromise = T | Promise; -declare module 'apollo-server-env/dist/fetch' { - interface RequestInit { - cacheOptions?: - | CacheOptions - | ((response: Response, request: Request) => CacheOptions | undefined); - } -} -export type RequestOptions = RequestInit & { - path: string; +// URLSearchParams is globally available in Node / coming from @types/node +type URLSearchParamsInit = ConstructorParameters[0]; + +export type RequestOptions = FetcherRequestInit & { + params?: URLSearchParamsInit; + cacheOptions?: + | CacheOptions + | (( + url: string, + response: FetcherResponse, + request: RequestOptions, + ) => CacheOptions | undefined); +}; + +export type WillSendRequestOptions = Omit< + WithRequired, + 'params' +> & { params: URLSearchParams; - headers: Headers; - body?: Body; }; +export interface GetRequest extends RequestOptions { + method?: 'GET'; + body?: never; +} + +export interface RequestWithBody extends Omit { + method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + body?: FetcherRequestInit['body'] | object; +} + +type DataSourceRequest = GetRequest | RequestWithBody; + export interface CacheOptions { ttl?: number; } -export type Body = BodyInit | object; -export { Request }; - const NODE_ENV = process.env.NODE_ENV; -export abstract class RESTDataSource extends DataSource { - httpCache!: HTTPCache; - context!: TContext; - memoizedResults = new Map>(); +export interface DataSourceConfig { + cache?: KeyValueCache; + fetch?: Fetcher; +} - constructor(private httpFetch?: typeof fetch) { - super(); - } +export abstract class RESTDataSource { + httpCache: HTTPCache; + memoizedResults = new Map>(); - override initialize(config: DataSourceConfig): void { - this.context = config.context; - this.httpCache = new HTTPCache(config.cache, this.httpFetch); + constructor(config?: DataSourceConfig) { + this.httpCache = new HTTPCache(config?.cache, config?.fetch); } baseURL?: string; @@ -66,14 +69,18 @@ export abstract class RESTDataSource extends DataSource { // For example, you could use this to take Vary header fields into account. // Although we do validate header fields and don't serve responses from cache when they don't match, // new responses overwrite old ones with different vary header fields. - protected cacheKeyFor(request: Request): string { - return request.url; + protected cacheKeyFor(url: URL, _request: RequestOptions): string { + return url.toString(); } - protected willSendRequest?(request: RequestOptions): ValueOrPromise; + protected willSendRequest?( + requestOpts: WillSendRequestOptions, + ): ValueOrPromise; - protected resolveURL(request: RequestOptions): ValueOrPromise { - let path = request.path; + protected resolveURL( + path: string, + _request: RequestOptions, + ): ValueOrPromise { if (path.startsWith('/')) { path = path.slice(1); } @@ -89,13 +96,14 @@ export abstract class RESTDataSource extends DataSource { } protected cacheOptionsFor?( - response: Response, - request: Request, + url: string, + response: FetcherResponse, + request: FetcherRequestInit, ): CacheOptions | undefined; protected async didReceiveResponse( - response: Response, - _request: Request, + response: FetcherResponse, + _request: RequestOptions, ): Promise { if (response.ok) { return this.parseBody(response) as any as Promise; @@ -104,11 +112,11 @@ export abstract class RESTDataSource extends DataSource { } } - protected didEncounterError(error: Error, _request: Request) { + protected didEncounterError(error: Error, _request: RequestOptions) { throw error; } - protected parseBody(response: Response): Promise { + protected parseBody(response: FetcherResponse): Promise { const contentType = response.headers.get('Content-Type'); const contentLength = response.headers.get('Content-Length'); if ( @@ -126,16 +134,16 @@ export abstract class RESTDataSource extends DataSource { } } - protected async errorFromResponse(response: Response) { + protected async errorFromResponse(response: FetcherResponse) { const message = `${response.status}: ${response.statusText}`; - let error: ApolloError; + let error: GraphQLError; if (response.status === 401) { error = new AuthenticationError(message); } else if (response.status === 403) { error = new ForbiddenError(message); } else { - error = new ApolloError(message); + error = new GraphQLError(message); } const body = await this.parseBody(response); @@ -154,119 +162,103 @@ export abstract class RESTDataSource extends DataSource { protected async get( path: string, - params?: URLSearchParamsInit, - init?: RequestInit, + request?: GetRequest, ): Promise { - return this.fetch( - Object.assign({ method: 'GET', path, params }, init), - ); + return this.fetch(path, { method: 'GET', ...request }); } protected async post( path: string, - body?: Body, - init?: RequestInit, + request?: RequestWithBody, ): Promise { - return this.fetch( - Object.assign({ method: 'POST', path, body }, init), - ); + return this.fetch(path, { method: 'POST', ...request }); } protected async patch( path: string, - body?: Body, - init?: RequestInit, + request?: RequestWithBody, ): Promise { - return this.fetch( - Object.assign({ method: 'PATCH', path, body }, init), - ); + return this.fetch(path, { method: 'PATCH', ...request }); } protected async put( path: string, - body?: Body, - init?: RequestInit, + request?: RequestWithBody, ): Promise { - return this.fetch( - Object.assign({ method: 'PUT', path, body }, init), - ); + return this.fetch(path, { method: 'PUT', ...request }); } protected async delete( path: string, - params?: URLSearchParamsInit, - init?: RequestInit, + request?: RequestWithBody, ): Promise { - return this.fetch( - Object.assign({ method: 'DELETE', path, params }, init), - ); + return this.fetch(path, { method: 'DELETE', ...request }); } private async fetch( - init: RequestInit & { - path: string; - params?: URLSearchParamsInit; - }, + path: string, + request: DataSourceRequest, ): Promise { - if (!(init.params instanceof URLSearchParams)) { - init.params = new URLSearchParams(init.params); - } - - if (!(init.headers instanceof Headers)) { - init.headers = new Headers(init.headers || Object.create(null)); - } - - const options = init as RequestOptions; + const modifiedRequest: WillSendRequestOptions = { + ...request, + // guarantee params and headers objects before calling `willSendRequest` for convenience + params: + request.params instanceof URLSearchParams + ? request.params + : new URLSearchParams(request.params), + headers: request.headers ?? Object.create(null), + body: undefined, + }; if (this.willSendRequest) { - await this.willSendRequest(options); + await this.willSendRequest(modifiedRequest); } - const url = await this.resolveURL(options); + const url = await this.resolveURL(path, modifiedRequest); // Append params to existing params in the path - for (const [name, value] of options.params) { + for (const [name, value] of modifiedRequest.params as URLSearchParams) { url.searchParams.append(name, value); } // We accept arbitrary objects and arrays as body and serialize them as JSON if ( - options.body !== undefined && - options.body !== null && - (options.body.constructor === Object || - Array.isArray(options.body) || - ((options.body as any).toJSON && - typeof (options.body as any).toJSON === 'function')) + request.body !== undefined && + request.body !== null && + (request.body.constructor === Object || + Array.isArray(request.body) || + ((request.body as any).toJSON && + typeof (request.body as any).toJSON === 'function')) ) { - options.body = JSON.stringify(options.body); + modifiedRequest.body = JSON.stringify(request.body); // If Content-Type header has not been previously set, set to application/json - if (!options.headers.get('Content-Type')) { - options.headers.set('Content-Type', 'application/json'); + if (!modifiedRequest.headers) { + modifiedRequest.headers = { 'content-type': 'application/json' }; + } else if (!modifiedRequest.headers['content-type']) { + modifiedRequest.headers['content-type'] = 'application/json'; } } - const request = new Request(String(url), options); - - const cacheKey = this.cacheKeyFor(request); + const cacheKey = this.cacheKeyFor(url, modifiedRequest); const performRequest = async () => { - return this.trace(request, async () => { - const cacheOptions = options.cacheOptions - ? options.cacheOptions + return this.trace(url, modifiedRequest, async () => { + const cacheOptions = modifiedRequest.cacheOptions + ? modifiedRequest.cacheOptions : this.cacheOptionsFor?.bind(this); try { - const response = await this.httpCache.fetch(request, { + const response = await this.httpCache.fetch(url, modifiedRequest, { cacheKey, cacheOptions, }); - return await this.didReceiveResponse(response, request); + return await this.didReceiveResponse(response, modifiedRequest); } catch (error) { - this.didEncounterError(error as Error, request); + this.didEncounterError(error as Error, modifiedRequest); } }); }; - if (request.method === 'GET') { + if (modifiedRequest.method === 'GET') { let promise = this.memoizedResults.get(cacheKey); if (promise) return promise; @@ -280,7 +272,8 @@ export abstract class RESTDataSource extends DataSource { } protected async trace( - request: Request, + url: URL, + request: RequestOptions, fn: () => Promise, ): Promise { if (NODE_ENV === 'development') { @@ -290,7 +283,7 @@ export abstract class RESTDataSource extends DataSource { return await fn(); } finally { const duration = Date.now() - startTime; - const label = `${request.method || 'GET'} ${request.url}`; + const label = `${request.method || 'GET'} ${url}`; console.log(`${label} (${duration}ms)`); } } else { @@ -298,3 +291,17 @@ export abstract class RESTDataSource extends DataSource { } } } + +export class AuthenticationError extends GraphQLError { + constructor(message: string) { + super(message, { extensions: { code: 'UNAUTHENTICATED' } }); + this.name = 'AuthenticationError'; + } +} + +export class ForbiddenError extends GraphQLError { + constructor(message: string) { + super(message, { extensions: { code: 'FORBIDDEN' } }); + this.name = 'ForbiddenError'; + } +} diff --git a/src/__tests__/HTTPCache.test.ts b/src/__tests__/HTTPCache.test.ts index 7a8677d..7dd4406 100644 --- a/src/__tests__/HTTPCache.test.ts +++ b/src/__tests__/HTTPCache.test.ts @@ -1,356 +1,338 @@ -import { fetch, Request } from './mock-apollo-server-env'; - -import FakeTimers from '@sinonjs/fake-timers'; - +import fetch from 'node-fetch'; +import nock from 'nock'; import { HTTPCache } from '../HTTPCache'; import { MapKeyValueCache } from './MapKeyValueCache'; +import { nockAfterEach, nockBeforeEach } from './nockAssertions'; describe('HTTPCache', () => { let store: MapKeyValueCache; let httpCache: HTTPCache; - let clock: FakeTimers.InstalledClock; - beforeAll(() => { - clock = FakeTimers.install(); + beforeEach(() => { + nockBeforeEach(); + store = new MapKeyValueCache(); + httpCache = new HTTPCache(store, fetch); }); - beforeEach(() => { - fetch.mockReset(); + afterEach(nockAfterEach); - store = new MapKeyValueCache(); - httpCache = new HTTPCache(store as any); + beforeAll(() => { + // nock depends on process.nextTick + jest.useFakeTimers({ doNotFake: ['nextTick'] }); }); afterAll(() => { - clock.uninstall(); + jest.useRealTimers(); }); - it('fetches a response from the origin when not cached', async () => { - fetch.mockJSONResponseOnce({ name: 'Ada Lovelace' }); + const apiUrl = 'https://api.example.com'; + const adaPath = '/people/1'; + const adaUrl = new URL(`${apiUrl}${adaPath}`); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + function mockGetAdaLovelace(headers: { [key: string]: string } = {}) { + return nock(apiUrl).get(adaPath).reply( + 200, + { + name: 'Ada Lovelace', + }, + headers, ); + } + + function mockGetAlanTuring(headers: { [key: string]: string } = {}) { + return nock(apiUrl).get(adaPath).reply( + 200, + { + name: 'Alan Turing', + }, + headers, + ); + } + + function mockInternalServerError(headers: { [key: string]: string } = {}) { + return nock(apiUrl) + .get(adaPath) + .reply(500, 'Internal Server Error', headers); + } + + it('fetches a response from the origin when not cached', async () => { + mockGetAdaLovelace(); + + const response = await httpCache.fetch(adaUrl); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); }); it('returns a cached response when not expired', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); - clock.tick(10000); + jest.advanceTimersByTime(10000); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), - ); + const response = await httpCache.fetch(adaUrl); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); - expect(response.headers.get('Age')).toEqual('10'); + expect(response.headers.get('age')).toEqual('10'); }); it('fetches a fresh response from the origin when expired', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); - clock.tick(30000); + jest.advanceTimersByTime(30000); - fetch.mockJSONResponseOnce( - { name: 'Alan Turing' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAlanTuring({ 'cache-control': 'max-age=30' }); const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + new URL('https://api.example.com/people/1'), ); - expect(fetch.mock.calls.length).toEqual(2); - expect(await response.json()).toEqual({ name: 'Alan Turing' }); - expect(response.headers.get('Age')).toEqual('0'); + expect(response.headers.get('age')).toEqual('0'); }); describe('overriding TTL', () => { it('returns a cached response when the overridden TTL is not expired', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, + mockGetAdaLovelace({ + 'cache-control': 'private, no-cache', + 'set-cookie': 'foo', + }); + + await httpCache.fetch( + adaUrl, + {}, { - 'Cache-Control': 'private, no-cache', - 'Set-Cookie': 'foo', + cacheOptions: { + ttl: 30, + }, }, ); - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: { - ttl: 30, - }, - }); - - clock.tick(10000); + jest.advanceTimersByTime(10000); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), - ); + const response = await httpCache.fetch(adaUrl); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); - expect(response.headers.get('Age')).toEqual('10'); + expect(response.headers.get('age')).toEqual('10'); }); it('fetches a fresh response from the origin when the overridden TTL expired', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { - 'Cache-Control': 'private, no-cache', - 'Set-Cookie': 'foo', - }, - ); - - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: { - ttl: 30, - }, + mockGetAdaLovelace({ + 'cache-control': 'private, no-cache', + 'set-cookie': 'foo', }); - clock.tick(30000); - - fetch.mockJSONResponseOnce( - { name: 'Alan Turing' }, + await httpCache.fetch( + adaUrl, + {}, { - 'Cache-Control': 'private, no-cache', - 'Set-Cookie': 'foo', + cacheOptions: { + ttl: 30, + }, }, ); + jest.advanceTimersByTime(30000); + + mockGetAlanTuring({ + 'cache-control': 'private, no-cache', + 'set-cookie': 'foo', + }); + const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + new URL('https://api.example.com/people/1'), ); - expect(fetch.mock.calls.length).toEqual(2); - expect(await response.json()).toEqual({ name: 'Alan Turing' }); - expect(response.headers.get('Age')).toEqual('0'); + expect(response.headers.get('age')).toEqual('0'); }); it('fetches a fresh response from the origin when the overridden TTL expired even if a longer max-age has been specified', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: { - ttl: 10, + await httpCache.fetch( + adaUrl, + {}, + { + cacheOptions: { + ttl: 10, + }, }, - }); + ); - clock.tick(10000); + jest.advanceTimersByTime(10000); - fetch.mockJSONResponseOnce( - { name: 'Alan Turing' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAlanTuring({ + 'cache-control': 'private, no-cache', + }); const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + new URL('https://api.example.com/people/1'), ); - expect(fetch.mock.calls.length).toEqual(2); - expect(await response.json()).toEqual({ name: 'Alan Turing' }); - expect(response.headers.get('Age')).toEqual('0'); + expect(response.headers.get('age')).toEqual('0'); }); it('does not store a response with an overridden TTL and a non-success status code', async () => { - fetch.mockResponseOnce( - 'Internal server error', - { 'Cache-Control': 'max-age=30' }, - 500, - ); + mockInternalServerError({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: { - ttl: 30, + await httpCache.fetch( + adaUrl, + {}, + { + cacheOptions: { + ttl: 30, + }, }, - }); + ); expect(store.size).toEqual(0); }); it('allows overriding the TTL dynamically', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, + mockGetAdaLovelace({ + 'cache-control': 'private, no-cache', + 'set-cookie': 'foo', + }); + + await httpCache.fetch( + adaUrl, + {}, { - 'Cache-Control': 'private, no-cache', - 'Set-Cookie': 'foo', + cacheOptions: () => ({ + ttl: 30, + }), }, ); - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: () => ({ - ttl: 30, - }), - }); - - clock.tick(10000); + jest.advanceTimersByTime(10000); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), - ); + const response = await httpCache.fetch(adaUrl); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); - expect(response.headers.get('Age')).toEqual('10'); + expect(response.headers.get('age')).toEqual('10'); }); it('allows disabling caching when the TTL is 0 (falsy)', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1'), { - cacheOptions: () => ({ - ttl: 0, - }), - }); + await httpCache.fetch( + adaUrl, + {}, + { + cacheOptions: () => ({ + ttl: 0, + }), + }, + ); expect(store.size).toEqual(0); }); }); it('allows specifying a custom cache key', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + nock(apiUrl) + .get(adaPath) + .query({ foo: '123' }) + .reply(200, { name: 'Ada Lovelace' }, { 'cache-control': 'max-age=30' }); await httpCache.fetch( - new Request('https://api.example.com/people/1?foo=bar'), - { cacheKey: 'https://api.example.com/people/1' }, + new URL(`${adaUrl}?foo=123`), + {}, + { cacheKey: adaUrl.toString() }, ); const response = await httpCache.fetch( - new Request('https://api.example.com/people/1?foo=baz'), - { cacheKey: 'https://api.example.com/people/1' }, + new URL(`${adaUrl}?foo=456`), + {}, + { cacheKey: adaUrl.toString() }, ); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); }); it('does not store a response to a non-GET request', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + nock(apiUrl) + .post(adaPath) + .reply(200, { name: 'Ada Lovelace' }, { 'cache-control': 'max-age=30' }); - await httpCache.fetch( - new Request('https://api.example.com/people/1', { method: 'POST' }), - ); + await httpCache.fetch(adaUrl, { method: 'POST' }); expect(store.size).toEqual(0); }); it('does not store a response with a non-success status code', async () => { - fetch.mockResponseOnce( - 'Internal server error', - { 'Cache-Control': 'max-age=30' }, - 500, - ); + mockInternalServerError({ 'cache-control': 'max-age=30' }); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); expect(store.size).toEqual(0); }); - it('does not store a response without Cache-Control header', async () => { - fetch.mockJSONResponseOnce({ name: 'Ada Lovelace' }); + it('does not store a response without cache-control header', async () => { + mockGetAdaLovelace(); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); expect(store.size).toEqual(0); }); it('does not store a private response', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'private, max-age: 60' }, - ); + mockGetAdaLovelace({ 'cache-control': 'private, max-age: 60' }); - await httpCache.fetch(new Request('https://api.example.com/me')); + await httpCache.fetch(adaUrl); expect(store.size).toEqual(0); }); - it('returns a cached response when Vary header fields match', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30', Vary: 'Accept-Language' }, - ); + it('returns a cached response when vary header fields match', async () => { + mockGetAdaLovelace({ + 'cache-control': 'max-age=30', + vary: 'Accept-Language', + }); - await httpCache.fetch( - new Request('https://api.example.com/people/1', { - headers: { 'Accept-Language': 'en' }, - }), - ); + await httpCache.fetch(adaUrl, { + headers: { 'accept-language': 'en' }, + }); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1', { - headers: { 'Accept-Language': 'en' }, - }), - ); + const response = await httpCache.fetch(adaUrl, { + headers: { 'accept-language': 'en' }, + }); - expect(fetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); }); - it(`does not return a cached response when Vary header fields don't match`, async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30', Vary: 'Accept-Language' }, - ); + it(`does not return a cached response when vary header fields don't match`, async () => { + mockGetAdaLovelace({ + 'cache-control': 'max-age=30', + vary: 'Accept-Language', + }); - await httpCache.fetch( - new Request('https://api.example.com/people/1', { - headers: { 'Accept-Language': 'en' }, - }), - ); + await httpCache.fetch(adaUrl, { + headers: { 'accept-language': 'en' }, + }); - fetch.mockJSONResponseOnce( - { name: 'Alan Turing' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAlanTuring({ 'cache-control': 'max-age=30' }); const response = await httpCache.fetch( - new Request('https://api.example.com/people/1', { - headers: { 'Accept-Language': 'fr' }, - }), + new URL('https://api.example.com/people/1'), + { + headers: { 'accept-language': 'fr' }, + }, ); - expect(fetch.mock.calls.length).toEqual(2); expect(await response.json()).toEqual({ name: 'Alan Turing' }); }); it('sets the TTL as max-age when the response does not contain revalidation headers', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30' }); const storeSet = jest.spyOn(store, 'set'); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); expect(storeSet).toHaveBeenCalledWith( expect.any(String), @@ -361,14 +343,11 @@ describe('HTTPCache', () => { }); it('sets the TTL as 2 * max-age when the response contains an ETag header', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { 'Cache-Control': 'max-age=30', ETag: 'foo' }, - ); + mockGetAdaLovelace({ 'cache-control': 'max-age=30', etag: 'foo' }); const storeSet = jest.spyOn(store, 'set'); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); expect(storeSet).toHaveBeenCalledWith( expect.any(String), @@ -380,110 +359,79 @@ describe('HTTPCache', () => { }); it('revalidates a cached response when expired and returns the cached response when not modified', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { - 'Cache-Control': 'public, max-age=30', - ETag: 'foo', - }, - ); - - await httpCache.fetch(new Request('https://api.example.com/people/1')); + mockGetAdaLovelace({ + 'cache-control': 'public, max-age=30', + etag: 'foo', + }); - clock.tick(30000); + await httpCache.fetch(adaUrl); - fetch.mockResponseOnce( - null, - { - 'Cache-Control': 'public, max-age=30', - ETag: 'foo', - }, - 304, - ); + jest.advanceTimersByTime(30000); - const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), - ); + nock(apiUrl) + .get(adaPath) + .matchHeader('if-none-match', 'foo') + .reply(304, undefined, { + 'cache-control': 'public, max-age=30', + etag: 'foo', + }); - expect(fetch.mock.calls.length).toEqual(2); - expect( - (fetch.mock.calls[1][0] as Request).headers.get('If-None-Match'), - ).toEqual('foo'); + const response = await httpCache.fetch(adaUrl); expect(response.status).toEqual(200); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); - expect(response.headers.get('Age')).toEqual('0'); + expect(response.headers.get('age')).toEqual('0'); - clock.tick(10000); + jest.advanceTimersByTime(10000); - const response2 = await httpCache.fetch( - new Request('https://api.example.com/people/1'), - ); - - expect(fetch.mock.calls.length).toEqual(2); + const response2 = await httpCache.fetch(adaUrl); expect(response2.status).toEqual(200); expect(await response2.json()).toEqual({ name: 'Ada Lovelace' }); - expect(response2.headers.get('Age')).toEqual('10'); + expect(response2.headers.get('age')).toEqual('10'); }); it('revalidates a cached response when expired and returns and caches a fresh response when modified', async () => { - fetch.mockJSONResponseOnce( - { name: 'Ada Lovelace' }, - { - 'Cache-Control': 'public, max-age=30', - ETag: 'foo', - }, - ); + mockGetAdaLovelace({ + 'cache-control': 'public, max-age=30', + etag: 'foo', + }); - await httpCache.fetch(new Request('https://api.example.com/people/1')); + await httpCache.fetch(adaUrl); - clock.tick(30000); + jest.advanceTimersByTime(30000); - fetch.mockJSONResponseOnce( - { name: 'Alan Turing' }, - { - 'Cache-Control': 'public, max-age=30', - ETag: 'bar', - }, - ); + mockGetAlanTuring({ + 'cache-control': 'public, max-age=30', + etag: 'bar', + }); const response = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + new URL('https://api.example.com/people/1'), ); - expect(fetch.mock.calls.length).toEqual(2); - expect( - (fetch.mock.calls[1][0] as Request).headers.get('If-None-Match'), - ).toEqual('foo'); - expect(response.status).toEqual(200); expect(await response.json()).toEqual({ name: 'Alan Turing' }); - clock.tick(10000); + jest.advanceTimersByTime(10000); const response2 = await httpCache.fetch( - new Request('https://api.example.com/people/1'), + new URL('https://api.example.com/people/1'), ); - expect(fetch.mock.calls.length).toEqual(2); - expect(response2.status).toEqual(200); expect(await response2.json()).toEqual({ name: 'Alan Turing' }); - expect(response2.headers.get('Age')).toEqual('10'); + expect(response2.headers.get('age')).toEqual('10'); }); it('fetches a response from the origin with a custom fetch function', async () => { - fetch.mockJSONResponseOnce({ name: 'Ada Lovelace' }); + mockGetAdaLovelace(); const customFetch = jest.fn(fetch); const customHttpCache = new HTTPCache(store as any, customFetch); - const response = await customHttpCache.fetch( - new Request('https://api.example.com/people/1'), - ); + const response = await customHttpCache.fetch(adaUrl); - expect(customFetch.mock.calls.length).toEqual(1); expect(await response.json()).toEqual({ name: 'Ada Lovelace' }); }); }); diff --git a/src/__tests__/RESTDataSource.test.ts b/src/__tests__/RESTDataSource.test.ts index 50761f5..334fb5e 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -1,97 +1,81 @@ -import { fetch, URL, Request } from './mock-apollo-server-env'; - +// import fetch, { Request } from 'node-fetch'; import { - ApolloError, AuthenticationError, + DataSourceConfig, ForbiddenError, -} from 'apollo-server-errors'; -import { RESTDataSource, RequestOptions } from '../RESTDataSource'; + RequestOptions, + RESTDataSource, + // RequestOptions +} from '../RESTDataSource'; + +// import { HTTPCache } from '../HTTPCache'; +// import { MapKeyValueCache } from './MapKeyValueCache'; +import { nockAfterEach, nockBeforeEach } from './nockAssertions'; +import nock from 'nock'; +import { GraphQLError } from 'graphql'; -import { HTTPCache } from '../HTTPCache'; -import { MapKeyValueCache } from './MapKeyValueCache'; +const apiUrl = 'https://api.example.com'; describe('RESTDataSource', () => { - let httpCache: HTTPCache; + // let httpCache: HTTPCache; beforeEach(() => { - httpCache = new HTTPCache(new MapKeyValueCache()); + nockBeforeEach(); + // httpCache = new HTTPCache(new MapKeyValueCache()); }); - beforeEach(() => { - fetch.mockReset(); - }); + afterEach(nockAfterEach); describe('constructing requests', () => { it('interprets paths relative to the base URL', async () => { const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + override baseURL = apiUrl; getFoo() { return this.get('foo'); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/foo').reply(200, {}); await dataSource.getFoo(); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo', - ); }); it('interprets paths with a leading slash relative to the base URL', async () => { const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com/bar'; + override baseURL = `${apiUrl}/bar`; getFoo() { return this.get('/foo'); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/bar/foo').reply(200, {}); await dataSource.getFoo(); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/bar/foo', - ); }); it('adds a trailing slash to the base URL if needed', async () => { const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://example.com/api'; + override baseURL = `${apiUrl}/api`; getFoo() { return this.get('foo'); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/api/foo').reply(200, {}); await dataSource.getFoo(); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://example.com/api/foo', - ); }); it('allows resolving a base URL asynchronously', async () => { const dataSource = new (class extends RESTDataSource { - override async resolveURL(request: RequestOptions) { + override async resolveURL(path: string, request: RequestOptions) { if (!this.baseURL) { this.baseURL = 'https://api.example.com'; } - return super.resolveURL(request); + return super.resolveURL(path, request); } getFoo() { @@ -99,15 +83,9 @@ describe('RESTDataSource', () => { } })(); - dataSource.httpCache = httpCache; + nock(apiUrl).get('/foo').reply(200, {}); - fetch.mockJSONResponseOnce(); await dataSource.getFoo(); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo', - ); }); it('allows passing in query string parameters', async () => { @@ -118,50 +96,58 @@ describe('RESTDataSource', () => { username: string, params: { filter: string; limit: number; offset: number }, ) { - return this.get('posts', Object.assign({ username }, params)); + return this.get('posts', { + params: { + username, + filter: params.filter, + limit: params.limit.toString(), + offset: params.offset.toString(), + }, + }); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl) + .get('/posts') + .query({ + username: 'beyoncé', + filter: 'jalapeño', + limit: 10, + offset: 20, + }) + .reply(200); await dataSource.getPostsForUser('beyoncé', { filter: 'jalapeño', limit: 10, offset: 20, }); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/posts?username=beyonc%C3%A9&filter=jalape%C3%B1o&limit=10&offset=20', - ); }); it('allows setting default query string parameters', async () => { - const dataSource = new (class extends RESTDataSource { + class AuthedDataSource extends RESTDataSource { override baseURL = 'https://api.example.com'; - override willSendRequest(request: RequestOptions) { - request.params.set('api_key', this.context.token); + constructor(private token: string, config?: DataSourceConfig) { + super(config); } - getFoo() { - return this.get('foo', { a: 1 }); + override willSendRequest(request: RequestOptions) { + const params = new URLSearchParams(request.params); + params.set('apiKey', this.token); + request.params = params; } - })(); - dataSource.context = { token: 'secret' }; - dataSource.httpCache = httpCache; + getFoo(id: string) { + return this.get('foo', { params: { id } }); + } + } - fetch.mockJSONResponseOnce(); + const dataSource = new AuthedDataSource('secret'); - await dataSource.getFoo(); + nock(apiUrl).get('/foo').query({ id: '1', apiKey: 'secret' }).reply(200); - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo?a=1&api_key=secret', - ); + await dataSource.getFoo('1'); }); it('allows setting default fetch options', async () => { @@ -169,7 +155,7 @@ describe('RESTDataSource', () => { override baseURL = 'https://api.example.com'; override willSendRequest(request: RequestOptions) { - request.credentials = 'include'; + request.headers = { ...request.headers, credentials: 'include' }; } getFoo() { @@ -177,89 +163,85 @@ describe('RESTDataSource', () => { } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/foo').matchHeader('credentials', 'include').reply(200); await dataSource.getFoo(); - - expect(fetch).toBeCalledTimes(1); - // TODO: request.credentials is not supported by node-fetch - // expect(fetch.mock.calls[0][0].credentials).toEqual('include'); }); it('allows setting request headers', async () => { - const dataSource = new (class extends RESTDataSource { + class AuthedDataSource extends RESTDataSource { override baseURL = 'https://api.example.com'; - override willSendRequest(request: RequestOptions) { - request.headers.set('Authorization', this.context.token); + constructor(private token: string, config?: DataSourceConfig) { + super(config); } - getFoo() { - return this.get('foo'); + override willSendRequest(request: RequestOptions) { + request.headers = { ...request.headers, authorization: this.token }; } - })(); - dataSource.context = { token: 'secret' }; - dataSource.httpCache = httpCache; + getFoo(id: string) { + return this.get('foo', { params: { id } }); + } + } - fetch.mockJSONResponseOnce(); + const dataSource = new AuthedDataSource('secret'); - await dataSource.getFoo(); + nock(apiUrl) + .get('/foo') + .query({ id: '1' }) + .matchHeader('authorization', 'secret') + .reply(200); - expect(fetch).toBeCalledTimes(1); - expect( - (fetch.mock.calls[0][0] as Request).headers.get('Authorization'), - ).toEqual('secret'); + await dataSource.getFoo('1'); }); - function expectJSONFetch(url: string, bodyJSON: unknown) { - expect(fetch).toBeCalledTimes(1); - const request = fetch.mock.calls[0][0] as Request; - expect(request.url).toEqual(url); - // request.body is a node-fetch extension which we aren't properly - // capturing in our TS types. - expect((request as any).body.toString()).toEqual( - JSON.stringify(bodyJSON), - ); - expect(request.headers.get('Content-Type')).toEqual('application/json'); - } + // function expectJSONFetch(url: string, bodyJSON: unknown) { + // expect(fetch).toBeCalledTimes(1); + // const request = fetch.mock.calls[0][0] as Request; + // expect(request.url).toEqual(url); + // // request.body is a node-fetch extension which we aren't properly + // // capturing in our TS types. + // expect((request as any).body.toString()).toEqual( + // JSON.stringify(bodyJSON), + // ); + // expect(request.headers.get('Content-Type')).toEqual('application/json'); + // } it('serializes a request body that is an object as JSON', async () => { + const expectedFoo = { foo: 'bar' }; const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; postFoo(foo: object) { - return this.post('foo', foo); + return this.post('foo', { body: foo }); } })(); - dataSource.httpCache = httpCache; + nock(apiUrl) + .post('/foo', expectedFoo) + .matchHeader('content-type', 'application/json') + .reply(200); - fetch.mockJSONResponseOnce(); - - await dataSource.postFoo({ foo: 'bar' }); - - expectJSONFetch('https://api.example.com/foo', { foo: 'bar' }); + await dataSource.postFoo(expectedFoo); }); it('serializes a request body that is an array as JSON', async () => { + const expected = ['foo', 'bar']; const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; postFoo(foo: string[]) { - return this.post('foo', foo); + return this.post('foo', { body: foo }); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); + nock(apiUrl) + .post('/foo', expected) + .matchHeader('content-type', 'application/json') + .reply(200); - await dataSource.postFoo(['foo', 'bar']); - - expectJSONFetch('https://api.example.com/foo', ['foo', 'bar']); + await dataSource.postFoo(expected); }); it('serializes a request body that has a toJSON method as JSON', async () => { @@ -267,14 +249,10 @@ describe('RESTDataSource', () => { override baseURL = 'https://api.example.com'; postFoo(foo: Model) { - return this.post('foo', foo); + return this.post('foo', { body: foo }); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); - class Model { constructor(public baz: any) {} @@ -286,9 +264,12 @@ describe('RESTDataSource', () => { } const model = new Model('bar'); - await dataSource.postFoo(model); + nock(apiUrl) + .post('/foo', { foo: 'bar' }) + .matchHeader('content-type', 'application/json') + .reply(200); - expectJSONFetch('https://api.example.com/foo', { foo: 'bar' }); + await dataSource.postFoo(model); }); it('does not serialize a request body that is not an object', async () => { @@ -296,31 +277,19 @@ describe('RESTDataSource', () => { override baseURL = 'https://api.example.com'; postFoo(foo: FormData) { - return this.post('foo', foo); + return this.post('foo', { body: foo }); } })(); - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); - class FormData {} const form = new FormData(); - await dataSource.postFoo(form); + nock(apiUrl).post('/foo').reply(200); - expect(fetch).toBeCalledTimes(1); - const request = fetch.mock.calls[0][0] as Request; - expect(request.url).toEqual('https://api.example.com/foo'); - // request.body is a node-fetch extension which we aren't properly - // capturing in our TS types. - expect((request as any).body.toString()).not.toEqual('{}'); - expect(request.headers.get('Content-Type')).not.toEqual( - 'application/json', - ); + await dataSource.postFoo(form); }); - for (const method of ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']) { + describe('all methods', () => { const dataSource = new (class extends RESTDataSource { override baseURL = 'https://api.example.com'; @@ -345,438 +314,413 @@ describe('RESTDataSource', () => { } })(); - it(`allows performing ${method} requests`, async () => { - dataSource.httpCache = httpCache; + const expectedFoo = { foo: 'bar' }; - fetch.mockJSONResponseOnce({ foo: 'bar' }); + it('GET', async () => { + nock(apiUrl).get('/foo').reply(200, expectedFoo); - const data = await (dataSource as any)[ - `${method.toLocaleLowerCase()}Foo` - ](); + const data = await dataSource.getFoo(); - expect(data).toEqual({ foo: 'bar' }); - - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).method).toEqual(method); + expect(data).toEqual(expectedFoo); }); - } - }); - describe('response parsing', () => { - it('returns data as parsed JSON when Content-Type is application/json', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + it('POST', async () => { + nock(apiUrl).post('/foo').reply(200, expectedFoo); - getFoo() { - return this.get('foo'); - } - })(); + const data = await dataSource.postFoo(); - dataSource.httpCache = httpCache; + expect(data).toEqual(expectedFoo); + }); - fetch.mockJSONResponseOnce( - { foo: 'bar' }, - { 'Content-Type': 'application/json' }, - ); + it('PATCH', async () => { + nock(apiUrl).patch('/foo').reply(200, expectedFoo); - const data = await dataSource.getFoo(); + const data = await dataSource.patchFoo(); - expect(data).toEqual({ foo: 'bar' }); - }); + expect(data).toEqual(expectedFoo); + }); - it('returns data as parsed JSON when Content-Type is application/hal+json', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + it('PUT', async () => { + nock(apiUrl).put('/foo').reply(200, expectedFoo); - getFoo() { - return this.get('foo'); - } - })(); + const data = await dataSource.putFoo(); - dataSource.httpCache = httpCache; + expect(data).toEqual(expectedFoo); + }); - fetch.mockJSONResponseOnce( - { foo: 'bar' }, - { 'Content-Type': 'application/hal+json' }, - ); + it('DELETE', async () => { + nock(apiUrl).delete('/foo').reply(200, expectedFoo); - const data = await dataSource.getFoo(); + const data = await dataSource.deleteFoo(); - expect(data).toEqual({ foo: 'bar' }); + expect(data).toEqual(expectedFoo); + }); }); - it('returns data as parsed JSON when Content-Type ends in +json', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + describe('response parsing', () => { + it('returns data as parsed JSON when Content-Type is application/json', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - })(); - - dataSource.httpCache = httpCache; + getFoo() { + return this.get('foo'); + } + })(); - fetch.mockJSONResponseOnce( - { foo: 'bar' }, - { 'Content-Type': 'application/vnd.api+json' }, - ); + nock(apiUrl) + .get('/foo') + .reply(200, { foo: 'bar' }, { 'content-type': 'application/json' }); - const data = await dataSource.getFoo(); + const data = await dataSource.getFoo(); - expect(data).toEqual({ foo: 'bar' }); - }); + expect(data).toEqual({ foo: 'bar' }); + }); - it('returns data as a string when Content-Type is text/plain', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + it('returns data as parsed JSON when Content-Type is application/hal+json', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - })(); - - dataSource.httpCache = httpCache; + getFoo() { + return this.get('foo'); + } + })(); - fetch.mockResponseOnce('bar', { 'Content-Type': 'text/plain' }); + nock(apiUrl) + .get('/foo') + .reply( + 200, + { foo: 'bar' }, + { 'content-type': 'application/hal+json' }, + ); - const data = await dataSource.getFoo(); + const data = await dataSource.getFoo(); - expect(data).toEqual('bar'); - }); + expect(data).toEqual({ foo: 'bar' }); + }); - it('attempts to return data as a string when no Content-Type header is returned', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + it('returns data as parsed JSON when Content-Type ends in +json', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - })(); + getFoo() { + return this.get('foo'); + } + })(); - dataSource.httpCache = httpCache; + nock(apiUrl) + .get('/foo') + .reply( + 200, + { foo: 'bar' }, + { 'content-type': 'application/vnd.api+json' }, + ); - fetch.mockResponseOnce('bar'); + const data = await dataSource.getFoo(); - const data = await dataSource.getFoo(); + expect(data).toEqual({ foo: 'bar' }); + }); - expect(data).toEqual('bar'); - }); + it('returns data as a string when Content-Type is text/plain', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - it('returns data as a string when response status code is 204 no content', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + getFoo() { + return this.get('foo'); + } + })(); - getFoo() { - return this.get(''); - } - })(); + nock(apiUrl) + .get('/foo') + .reply(200, 'bar', { 'content-type': 'text/plain' }); - dataSource.httpCache = httpCache; + const data = await dataSource.getFoo(); - fetch.mockResponseOnce('', { 'Content-Type': 'application/json' }, 204); + expect(data).toEqual('bar'); + }); - const data = await dataSource.getFoo(); + it('attempts to return data as a string when no Content-Type header is returned', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - expect(data).toEqual(''); - }); - - it('returns empty object when response content length is 0', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + getFoo() { + return this.get('foo'); + } + })(); - getFoo() { - return this.get(''); - } - })(); + nock(apiUrl).get('/foo').reply(200, 'bar'); - dataSource.httpCache = httpCache; + const data = await dataSource.getFoo(); - fetch.mockResponseOnce('', { - 'Content-Type': 'application/json', - 'Content-Length': '0', + expect(data).toEqual('bar'); }); - const data = await dataSource.getFoo(); + it('returns data as a string when response status code is 204 no content', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - expect(data).toEqual(''); - }); - }); - - describe('memoization', () => { - it('deduplicates requests with the same cache key', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; - - getFoo(id: number) { - return this.get(`foo/${id}`); - } - })(); - - dataSource.httpCache = httpCache; + getFoo() { + return this.get(''); + } + })(); - fetch.mockJSONResponseOnce(); + nock(apiUrl) + .get('/') + .reply(204, '', { 'content-type': 'application/json' }); - await Promise.all([dataSource.getFoo(1), dataSource.getFoo(1)]); + const data = await dataSource.getFoo(); - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo/1', - ); - }); - - it('does not deduplicate requests with a different cache key', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + expect(data).toEqual(''); + }); - getFoo(id: number) { - return this.get(`foo/${id}`); - } - })(); + it('returns empty object when response content length is 0', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; + getFoo() { + return this.get(''); + } + })(); - fetch.mockJSONResponseOnce(); - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/').reply(200, '', { + 'content-type': 'application/json', + 'content-length': '0', + }); - await Promise.all([dataSource.getFoo(1), dataSource.getFoo(2)]); + const data = await dataSource.getFoo(); - expect(fetch.mock.calls.length).toEqual(2); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo/1', - ); - expect((fetch.mock.calls[1][0] as Request).url).toEqual( - 'https://api.example.com/foo/2', - ); + expect(data).toEqual(''); + }); }); - it('does not deduplicate non-GET requests', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + describe('memoization', () => { + it('deduplicates requests with the same cache key', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - postFoo(id: number) { - return this.post(`foo/${id}`); - } - })(); + getFoo(id: number) { + return this.get(`foo/${id}`); + } + })(); - dataSource.httpCache = httpCache; + nock(apiUrl).get('/foo/1').reply(200); - fetch.mockJSONResponseOnce(); - fetch.mockJSONResponseOnce(); + await Promise.all([dataSource.getFoo(1), dataSource.getFoo(1)]); + }); - await Promise.all([dataSource.postFoo(1), dataSource.postFoo(1)]); + it('does not deduplicate requests with a different cache key', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - expect(fetch.mock.calls.length).toEqual(2); - }); + getFoo(id: number) { + return this.get(`foo/${id}`); + } + })(); - it('non-GET request removes memoized request with the same cache key', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + nock(apiUrl).get('/foo/1').reply(200); + nock(apiUrl).get('/foo/2').reply(200); - getFoo(id: number) { - return this.get(`foo/${id}`); - } + await Promise.all([dataSource.getFoo(1), dataSource.getFoo(2)]); + }); - postFoo(id: number) { - return this.post(`foo/${id}`); - } - })(); + it('does not deduplicate non-GET requests', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; - - fetch.mockJSONResponseOnce(); - fetch.mockJSONResponseOnce(); - fetch.mockJSONResponseOnce(); - - await Promise.all([ - dataSource.getFoo(1), - dataSource.postFoo(1), - dataSource.getFoo(1), - ]); - - expect(fetch.mock.calls.length).toEqual(3); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo/1', - ); - expect((fetch.mock.calls[2][0] as Request).url).toEqual( - 'https://api.example.com/foo/1', - ); - }); + postFoo(id: number) { + return this.post(`foo/${id}`); + } + })(); - it('allows specifying a custom cache key', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + nock(apiUrl).post('/foo/1').reply(200); + nock(apiUrl).post('/foo/1').reply(200); - override cacheKeyFor(request: Request) { - const url = new URL(request.url); - url.search = ''; - return url.toString(); - } + await Promise.all([dataSource.postFoo(1), dataSource.postFoo(1)]); + }); - getFoo(id: number, apiKey: string) { - return this.get(`foo/${id}`, { api_key: apiKey }); - } - })(); + it('non-GET request removes memoized request with the same cache key', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; + getFoo(id: number) { + return this.get(`foo/${id}`); + } - fetch.mockJSONResponseOnce(); + postFoo(id: number) { + return this.post(`foo/${id}`); + } + })(); - await Promise.all([ - dataSource.getFoo(1, 'secret'), - dataSource.getFoo(1, 'anotherSecret'), - ]); + nock(apiUrl).get('/foo/1').reply(200); + nock(apiUrl).post('/foo/1').reply(200); + nock(apiUrl).get('/foo/1').reply(200); - expect(fetch).toBeCalledTimes(1); - expect((fetch.mock.calls[0][0] as Request).url).toEqual( - 'https://api.example.com/foo/1?api_key=secret', - ); - }); - }); + await Promise.all([ + dataSource.getFoo(1), + dataSource.postFoo(1), + dataSource.getFoo(1), + ]); + }); - describe('error handling', () => { - it('throws an AuthenticationError when the response status is 401', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + it('allows specifying a custom cache key', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - })(); + override cacheKeyFor(url: URL, _request: RequestOptions) { + const urlNoSearchParams = new URL(url); + urlNoSearchParams.search = ''; + return urlNoSearchParams.toString(); + } - dataSource.httpCache = httpCache; + getFoo(id: number, apiKey: string) { + return this.get(`foo/${id}`, { + params: { api_key: apiKey }, + }); + } + })(); - fetch.mockResponseOnce('Invalid token', undefined, 401); + nock(apiUrl).get('/foo/1').query({ api_key: 'secret' }).reply(200); - const result = dataSource.getFoo(); - await expect(result).rejects.toThrow(AuthenticationError); - await expect(result).rejects.toMatchObject({ - extensions: { - code: 'UNAUTHENTICATED', - response: { - status: 401, - body: 'Invalid token', - }, - }, + await Promise.all([ + dataSource.getFoo(1, 'secret'), + dataSource.getFoo(1, 'anotherSecret'), + ]); }); }); - it('throws a ForbiddenError when the response status is 403', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; - - getFoo() { - return this.get('foo'); - } - })(); + describe('error handling', () => { + it('throws an AuthenticationError when the response status is 401', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; + getFoo() { + return this.get('foo'); + } + })(); + + nock(apiUrl).get('/foo').reply(401, 'Invalid token'); + + const result = dataSource.getFoo(); + await expect(result).rejects.toThrow(AuthenticationError); + await expect(result).rejects.toMatchObject({ + extensions: { + code: 'UNAUTHENTICATED', + response: { + status: 401, + body: 'Invalid token', + }, + }, + }); + }); - fetch.mockResponseOnce('No access', undefined, 403); + it('throws a ForbiddenError when the response status is 403', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - const result = dataSource.getFoo(); - await expect(result).rejects.toThrow(ForbiddenError); - await expect(result).rejects.toMatchObject({ - extensions: { - code: 'FORBIDDEN', - response: { - status: 403, - body: 'No access', + getFoo() { + return this.get('foo'); + } + })(); + + nock(apiUrl).get('/foo').reply(403, 'No access'); + + const result = dataSource.getFoo(); + await expect(result).rejects.toThrow(ForbiddenError); + await expect(result).rejects.toMatchObject({ + extensions: { + code: 'FORBIDDEN', + response: { + status: 403, + body: 'No access', + }, }, - }, + }); }); - }); - - it('throws an ApolloError when the response status is 500', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - })(); + it('throws an ApolloError when the response status is 500', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; + getFoo() { + return this.get('foo'); + } + })(); - fetch.mockResponseOnce('Oops', undefined, 500); + nock(apiUrl).get('/foo').reply(500, 'Oops'); - const result = dataSource.getFoo(); - await expect(result).rejects.toThrow(ApolloError); - await expect(result).rejects.toMatchObject({ - extensions: { - response: { - status: 500, - body: 'Oops', + const result = dataSource.getFoo(); + await expect(result).rejects.toThrow(GraphQLError); + await expect(result).rejects.toMatchObject({ + extensions: { + response: { + status: 500, + body: 'Oops', + }, }, - }, + }); }); - }); - it('puts JSON error responses on the error as an object', async () => { - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; - - getFoo() { - return this.get('foo'); - } - })(); + it('puts JSON error responses on the error as an object', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - dataSource.httpCache = httpCache; + getFoo() { + return this.get('foo'); + } + })(); - fetch.mockResponseOnce( - JSON.stringify({ - errors: [ + nock(apiUrl) + .get('/foo') + .reply( + 500, { - message: 'Houston, we have a problem.', + errors: [{ message: 'Houston, we have a problem.' }], }, - ], - }), - { 'Content-Type': 'application/json' }, - 500, - ); - - const result = dataSource.getFoo(); - await expect(result).rejects.toThrow(ApolloError); - await expect(result).rejects.toMatchObject({ - extensions: { - response: { - status: 500, - body: { - errors: [ - { - message: 'Houston, we have a problem.', - }, - ], + { 'content-type': 'application/json' }, + ); + + const result = dataSource.getFoo(); + await expect(result).rejects.toThrow(GraphQLError); + await expect(result).rejects.toMatchObject({ + extensions: { + response: { + status: 500, + body: { + errors: [ + { + message: 'Houston, we have a problem.', + }, + ], + }, }, }, - }, + }); }); }); - }); - describe('trace', () => { - it('is called once per request', async () => { - const traceMock = jest.fn(); - const dataSource = new (class extends RESTDataSource { - override baseURL = 'https://api.example.com'; + describe('trace', () => { + it('is called once per request', async () => { + const dataSource = new (class extends RESTDataSource { + override baseURL = 'https://api.example.com'; - getFoo() { - return this.get('foo'); - } - - override trace = traceMock; - })(); + getFoo() { + return this.get('foo'); + } + })(); - dataSource.httpCache = httpCache; + // @ts-ignore TS doesn't recognize the `trace` property on `RESTDataSource` + const traceSpy = jest.spyOn(dataSource, 'trace'); - fetch.mockJSONResponseOnce(); + nock(apiUrl).get('/foo').reply(200); - await dataSource.getFoo(); + await dataSource.getFoo(); - expect(traceMock).toBeCalledTimes(1); - expect(traceMock).toBeCalledWith( - expect.any(Object), - expect.any(Function), - ); + expect(traceSpy).toBeCalledTimes(1); + expect(traceSpy).toBeCalledWith( + expect.any(URL), + expect.any(Object), + expect.any(Function), + ); + }); }); }); }); diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts new file mode 100644 index 0000000..b595f23 --- /dev/null +++ b/src/__tests__/integration.test.ts @@ -0,0 +1,62 @@ +import { ApolloServer } from '@apollo/server'; +import { RESTDataSource } from '../RESTDataSource'; + +const typeDefs = `#graphql + type Query { + foo: String + } + + type Mutation { + createFoo: Int! + } +`; + +describe('Works with ApolloServer', () => { + it('DataSources can be passed via `executeOperation` context argument and used in a resolver ', async () => { + let fooPosted = false; + class FooDS extends RESTDataSource { + override baseURL = 'https://api.example.com'; + + postFoo(foo: { id: number }) { + fooPosted = true; + return foo.id; + } + } + + interface MyContext { + dataSources: { + foo: FooDS; + }; + } + + const server = new ApolloServer({ + typeDefs, + resolvers: { + Mutation: { + createFoo(_, __, context) { + return context.dataSources.foo.postFoo({ id: 1 }); + }, + }, + }, + }); + await server.start(); + + const context: MyContext = { + dataSources: { + foo: new FooDS(), + }, + }; + + const res = await server.executeOperation( + { + query: `#graphql + mutation { createFoo } + `, + }, + context, + ); + + expect(fooPosted).toBe(true); + expect(res.result.data?.createFoo).toBe(1); + }); +}); diff --git a/src/__tests__/mock-apollo-server-env.ts b/src/__tests__/mock-apollo-server-env.ts deleted file mode 100644 index 648e89d..0000000 --- a/src/__tests__/mock-apollo-server-env.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - fetch, - Request, - Response, - Body, - BodyInit, - Headers, - HeadersInit, - URL, - URLSearchParams, -} from 'apollo-server-env'; - -interface FetchMock extends jest.MockedFunction { - mockResponseOnce(data?: any, headers?: HeadersInit, status?: number): this; - mockJSONResponseOnce(data?: object, headers?: HeadersInit): this; -} - -const mockFetch = jest.fn(fetch) as unknown as FetchMock; - -mockFetch.mockResponseOnce = ( - data?: BodyInit, - headers?: Headers, - status: number = 200, -) => { - return mockFetch.mockImplementationOnce(async () => { - return new Response(data, { - status, - headers, - }); - }); -}; - -mockFetch.mockJSONResponseOnce = ( - data = {}, - headers?: Headers, - status?: number, -) => { - return mockFetch.mockResponseOnce( - JSON.stringify(data), - Object.assign({ 'Content-Type': 'application/json' }, headers), - status, - ); -}; - -const env = { - fetch: mockFetch, - Request, - Response, - Body, - Headers, - URL, - URLSearchParams, -}; - -jest.doMock('apollo-server-env', () => env); - -export { - mockFetch as fetch, - Request, - Response, - Body, - Headers, - URL, - URLSearchParams, -}; diff --git a/src/__tests__/nockAssertions.ts b/src/__tests__/nockAssertions.ts new file mode 100644 index 0000000..4d542c1 --- /dev/null +++ b/src/__tests__/nockAssertions.ts @@ -0,0 +1,19 @@ +import nock from 'nock'; +// Ensures an active and clean nock before every test +export function nockBeforeEach() { + if (!nock.isActive()) { + nock.activate(); + } + // Cleaning _before_ each test ensures that any mocks from a previous test + // which failed don't affect the current test. + nock.cleanAll(); +} + +// Ensures a test is complete (all expected requests were run) and a clean +// global state after each test. +export function nockAfterEach() { + // unmock HTTP interceptor + nock.restore(); + // effectively nock.isDone() but with more helpful messages in test failures + expect(nock.activeMocks()).toEqual([]); +} diff --git a/src/__tests__/tsconfig.json b/src/__tests__/tsconfig.json deleted file mode 100644 index d7cd9b7..0000000 --- a/src/__tests__/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.test.base", - "include": ["**/*"], - "references": [ - { "path": "../../" }, - ] -} diff --git a/src/index.ts b/src/index.ts index b01b4b3..31328e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ -export { RESTDataSource, RequestOptions } from './RESTDataSource'; -export { HTTPCache } from './HTTPCache'; -export { Request, Response } from 'apollo-server-env'; +export { + RESTDataSource, + RequestOptions, + WillSendRequestOptions, +} from './RESTDataSource';