diff --git a/.circleci/config.yml b/.circleci/config.yml index 6f98693ad..bd09a655e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,13 +1,112 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference version: 2.1 -# Use a package of configuration called an orb. + +# These "CircleCI Orbs" are reusable bits of configuration that can be shared +# across projects. See https://circleci.com/orbs/ for more information. orbs: - # Declare a dependency on the welcome-orb - welcome: circleci/welcome-orb@0.4.1 -# Orchestrate or schedule a set of jobs + # `oss` is a local reference to the package. The source for Apollo Orbs can + # be found at http://github.com/apollographql/CircleCI-Orbs/. + oss: apollo/oss-ci-cd-tooling@0.0.5 + +commands: + # These are the steps used for each version of Node which we're testing + # against. Thanks to YAMLs inability to merge arrays (though it is able + # to merge objects), every version of Node must use the exact same steps, + # or these steps would need to be repeated in a version of Node that needs + # something different. Probably best to avoid that, out of principle, though. + common_test_steps: + description: "Commands to run on every Node.js environment" + steps: + - oss/install_specific_npm_version + - checkout + - oss/npm_clean_install_with_caching + - run: + command: npm run test:ci + environment: + JEST_JUNIT_OUTPUT_DIR: "reports/junit/" + - run: npm run coverage:upload + - store_test_results: + path: reports/junit + - store_artifacts: + path: reports/junit + +# Important! When adding a new job to `jobs`, make sure to define when it +# executes by also adding it to the `workflows` section below! +jobs: + # Platform tests, each with the same tests but different platform or version. + # The docker tag represents the Node.js version and the full list is available + # at https://hub.docker.com/r/circleci/node/. + + NodeJS 12: + executor: { name: oss/node, tag: '12' } + steps: + - common_test_steps + # We will save the results of this one particular invocation to use in + # the publish step. Not only does this make the publishing step take less + # time, this also ensures that a passing version gets deployed even if, + # theoretically, rebuilding the same commit on the same version of + # Node.js should yield the same results! + - persist_to_workspace: + root: . + paths: + - ./** + + NodeJS 14: + executor: { name: oss/node, tag: '14' } + steps: + - common_test_steps + +# XXX We used to use this filter to only run a "Docs" job on docs branches. +# Now we use it to disable all jobs. It's unclear if there's a simpler way +# to do this! +common_non_publish_filters: &common_non_publish_filters + filters: + branches: + # If 'docs' is found, with word boundaries on either side, skip. + ignore: /.*?\bdocs\b.*/ + # Ensure every job has `tags` filters since the publish steps have tags. + # This is some wild configuration thing that's pretty hard to figure out. + tags: + only: /.*/ + +common_publish_filters: &common_publish_filters + filters: + # Only run pre-publish and publish steps on specific tags. + tags: + only: /^publish\/[0-9]+$/ + # We want the publish to trigger on the above tag, not any branch. + branches: + ignore: /.*/ + workflows: - # Name the workflow "welcome" - welcome: - # Run the welcome/run job in its own container + version: 2 + Build: jobs: - - welcome/run + - NodeJS 12: + name: "JS: Node 12" + <<: *common_non_publish_filters + - NodeJS 14: + name: "JS: Node 14" + <<: *common_non_publish_filters + - oss/lerna_tarballs: + name: "JS: Package tarballs" + <<: *common_non_publish_filters + requires: + - "JS: Node 12" + - "JS: Node 14" + - oss/dry_run: + name: "JS: Dry-run" + <<: *common_publish_filters + requires: + - "JS: Node 12" + - "JS: Node 14" + - oss/confirmation: + name: "JS: Confirmation" + type: approval + <<: *common_publish_filters + requires: + - "JS: Dry-run" + - oss/publish: + name: "JS: Publish" + <<: *common_publish_filters + requires: + - "JS: Confirmation" diff --git a/.gitignore b/.gitignore index 52e48fb80..4b615a4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,25 @@ target/ # These are backup files generated by rustfmt **/*.rs.bk + +# JS: Ignore the compiled output. +dist/ + +# JS: TypeScript incremental compilation cache +*.tsbuildinfo + +# JS: npm error output debug logs +npm-debug.log* + +# JS: Jest Coverage +coverage/ + +# JS: JUnit Reports (used mainly in CircleCI) +reports/ +junit.xml + +# JS: Node modules +node_modules/ + +# Mac OS +.DS_Store diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..86a0b3b60 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +*.json +*.md +*.snap + +dist/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..afae53f9d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,26 @@ +module.exports = { + // Unfortunately, prettierrc doesn't support explicitly enumerating the files + // we wish to "prettify", instead relying on them being passed as a glob on + // the CLI. See https://github.com/prettier/prettier/issues/3764. + // + // Unfortunately, that complicates the `package.json` scripts since it + // requires duplicating globs in multiple places, and also prevents + // Prettier-enabled editors from knowing what files are to be covered. + // + // We can DRY this up a bit by leveraging "requirePragma", an instruction + // that tells prettier to only prettify files which contain `@prettier` + // (which none of the files in this repository have) and then specifying the + // exact files to be prettified. As the issue above notes, this should become + // more succinct in Prettier 2.x. + requirePragma: true, + overrides: [ + { + files: '{docs/{,source/**},.,{gateway-js,federation-js,federation-integration-testsuite-js}/**,test}/{*.js,*.ts}', + options: { + requirePragma: false, + trailingComma: 'all', + singleQuote: true, + }, + }, + ], +}; diff --git a/.vscode/.gitattributes b/.vscode/.gitattributes new file mode 100644 index 000000000..92b950c32 --- /dev/null +++ b/.vscode/.gitattributes @@ -0,0 +1 @@ +*.json linguist-language=JSON5 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..089e9fd8a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "[typescript]": { + "editor.tabSize": 2, + // You're welcome to use Prettier on code hunks that are directly related to + // your changes but we ask that, rather than using "Format Document" or + // `editor.formatOnSave`, you instead highlight your changes and choose + // "Format Selection" (Windows: Ctrl-K Ctrl-F; Mac: Cmd-K Cmd-F) to avoid + // destructive formatting changes to entire files. Thanks for understanding! + "editor.formatOnSave": false, + "editor.rulers": [80], + "editor.wordWrapColumn": 80, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "typescript.tsdk": "./node_modules/typescript/lib" + } +} diff --git a/LICENSE b/LICENSE index 455ef4bdc..e1cbfefd6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2020 Apollo GraphQL +Copyright (c) 2020 Apollo Graph, Inc. +Copyright (c) 2020 Meteor Development Group, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/federation-integration-testsuite-js/.gitignore b/federation-integration-testsuite-js/.gitignore new file mode 100644 index 000000000..93d3f513b --- /dev/null +++ b/federation-integration-testsuite-js/.gitignore @@ -0,0 +1,2 @@ +# JS: Ignore the compiled output. +dist/ diff --git a/federation-integration-testsuite-js/.npmignore b/federation-integration-testsuite-js/.npmignore new file mode 100644 index 000000000..a165046d3 --- /dev/null +++ b/federation-integration-testsuite-js/.npmignore @@ -0,0 +1,6 @@ +* +!src/**/* +!dist/**/* +dist/**/*.test.* +!package.json +!README.md diff --git a/federation-integration-testsuite-js/LICENSE.md b/federation-integration-testsuite-js/LICENSE.md new file mode 100644 index 000000000..c177b435f --- /dev/null +++ b/federation-integration-testsuite-js/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2020- Apollo Graph, Inc. +Copyright (c) 2019-2020 Meteor Development Group, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json new file mode 100644 index 000000000..c721f5697 --- /dev/null +++ b/federation-integration-testsuite-js/package.json @@ -0,0 +1,27 @@ +{ + "name": "apollo-federation-integration-testsuite", + "private": true, + "version": "0.20.1-alpha.0", + "description": "Apollo Federation Integrations / Test Fixtures", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/apollographql/federation", + "directory": "federation-integration-testsuite-js/" + }, + "keywords": [], + "author": "Apollo ", + "license": "MIT", + "bugs": { + "url": "https://github.com/apollographql/federation/issues" + }, + "homepage": "https://github.com/apollographql/federation#readme", + "engines": { + "node": ">=12" + }, + "dependencies": { + "apollo-graphql": "^0.6.0", + "graphql-tag": "^2.10.4" + } +} diff --git a/federation-integration-testsuite-js/src/fixtures/accounts.ts b/federation-integration-testsuite-js/src/fixtures/accounts.ts new file mode 100644 index 000000000..703452e17 --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/accounts.ts @@ -0,0 +1,158 @@ +import gql from 'graphql-tag'; +import { GraphQLResolverMap } from 'apollo-graphql'; + +export const name = 'accounts'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + schema { + query: RootQuery + mutation: Mutation + } + + extend type RootQuery { + user(id: ID!): User + me: User + } + + type PasswordAccount @key(fields: "email") { + email: String! + } + + type SMSAccount @key(fields: "number") { + number: String + } + + union AccountType = PasswordAccount | SMSAccount + + type UserMetadata { + name: String + address: String + description: String + } + + type User @key(fields: "id") @key(fields: "username name { first last }"){ + id: ID! + name: Name + username: String + birthDate(locale: String): String + account: AccountType + metadata: [UserMetadata] + } + + type Name { + first: String + last: String + } + + type Mutation { + login(username: String!, password: String!): User + } + + extend type Library @key(fields: "id") { + id: ID! @external + name: String @external + userAccount(id: ID! = "1"): User @requires(fields: "name") + } +`; + +const users = [ + { + id: '1', + name: { + first: 'Ada', + last: 'Lovelace' + }, + birthDate: '1815-12-10', + username: '@ada', + account: { __typename: 'LibraryAccount', id: '1' }, + }, + { + id: '2', + name: { + first: 'Alan', + last: 'Turing' + }, + birthDate: '1912-06-23', + username: '@complete', + account: { __typename: 'SMSAccount', number: '8675309' }, + }, +]; + +const metadata = [ + { + id: '1', + metadata: [{ name: 'meta1', address: '1', description: '2' }], + }, + { + id: '2', + metadata: [{ name: 'meta2', address: '3', description: '4' }], + }, +]; + +const libraryUsers: { [name: string]: string[] } = { + 'NYC Public Library': ['1', '2'], +}; + +export const resolvers: GraphQLResolverMap = { + RootQuery: { + user(_, args) { + return { id: args.id }; + }, + + me() { + return { id: '1' }; + }, + }, + User: { + __resolveObject(object) { + // Nested key example for @key(fields: "username name { first last }") + if (object.username && object.name.first && object.name.last) { + users.find(user => user.username === object.username); + } + + return users.find(user => user.id === object.id); + }, + birthDate(user, args) { + return args.locale + ? new Date(user.birthDate).toLocaleDateString(args.locale, { + timeZone: 'Asia/Samarkand', // UTC + 5 + }) + : user.birthDate; + }, + metadata(object) { + const metaIndex = metadata.findIndex(m => m.id === object.id); + return metadata[metaIndex].metadata.map(obj => ({ name: obj.name })); + }, + }, + UserMetadata: { + address(object) { + const metaIndex = metadata.findIndex(m => + m.metadata.find(o => o.name === object.name), + ); + return metadata[metaIndex].metadata[0].address; + }, + description(object) { + const metaIndex = metadata.findIndex(m => + m.metadata.find(o => o.name === object.name), + ); + return metadata[metaIndex].metadata[0].description; + }, + }, + Library: { + userAccount({ name }, { id: userId }) { + const libraryUserIds = libraryUsers[name]; + return libraryUserIds && + libraryUserIds.find((id: string) => id === userId) + ? { id: userId } + : null; + }, + }, + Mutation: { + login(_, args) { + return users.find(user => user.username === args.username); + }, + }, +}; diff --git a/federation-integration-testsuite-js/src/fixtures/books.ts b/federation-integration-testsuite-js/src/fixtures/books.ts new file mode 100644 index 000000000..4565214a2 --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/books.ts @@ -0,0 +1,129 @@ +import gql from 'graphql-tag'; +import { GraphQLResolverMap } from 'apollo-graphql'; + +export const name = 'books'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + extend type Query { + book(isbn: String!): Book + books: [Book] + library(id: ID!): Library + } + + type Library @key(fields: "id") { + id: ID! + name: String + } + + # FIXME: turn back on when unions are supported in composition + # type LibraryAccount @key(fields: "id") { + # id: ID! + # library: Library + # } + + # extend union AccountType = LibraryAccount + + type Book @key(fields: "isbn") { + isbn: String! + title: String + year: Int + similarBooks: [Book]! + metadata: [MetadataOrError] + } + + # Value type + type KeyValue { + key: String! + value: String! + } + + # Value type + type Error { + code: Int + message: String + } + + # Value type + union MetadataOrError = KeyValue | Error +`; + +const libraries = [{ id: '1', name: 'NYC Public Library' }]; +const books = [ + { + isbn: '0262510871', + title: 'Structure and Interpretation of Computer Programs', + year: 1996, + metadata: [{ key: 'Condition', value: 'excellent' }], + }, + { + isbn: '0136291554', + title: 'Object Oriented Software Construction', + year: 1997, + metadata: [ + { key: 'Condition', value: 'used' }, + { code: '401', message: 'Unauthorized' }, + ], + }, + { + isbn: '0201633612', + title: 'Design Patterns', + year: 1995, + similarBooks: ['0201633612', '0136291554'], + metadata: [{ key: 'Condition', value: 'like new' }], + }, + { + isbn: '1234567890', + title: 'The Year Was Null', + year: null, + }, + { + isbn: '404404404', + title: '', + year: 404, + }, + { + isbn: '0987654321', + title: 'No Books Like This Book!', + year: 2019, + similarBooks: ['', null], + }, +]; + +export const resolvers: GraphQLResolverMap = { + Book: { + __resolveObject(object) { + return books.find(book => book.isbn === object.isbn); + }, + similarBooks(object) { + return object.similarBooks + ? object.similarBooks + .map((isbn: string) => books.find(book => book.isbn === isbn)) + .filter(Boolean) + : []; + }, + }, + Library: { + __resolveReference(object) { + return libraries.find(library => library.id === object.id); + }, + }, + Query: { + book(_, args) { + return { isbn: args.isbn }; + }, + books() { + return books; + }, + library(_, { id }) { + return libraries.find(library => library.id === id); + }, + }, + MetadataOrError: { + __resolveType(object) { + return 'key' in object ? 'KeyValue' : 'Error'; + }, + }, +}; diff --git a/federation-integration-testsuite-js/src/fixtures/documents.ts b/federation-integration-testsuite-js/src/fixtures/documents.ts new file mode 100644 index 000000000..a237449ec --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/documents.ts @@ -0,0 +1,35 @@ +import gql from 'graphql-tag'; + +export const name = 'documents'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + extend type Query { + body: Body! + } + + union Body = Image | Text + + type Image { + name: String! + # Same as option below but the type is different + attributes: ImageAttributes! + } + + type Text { + name: String! + # Same as option above but the type is different + attributes: TextAttributes! + } + + type ImageAttributes { + url: String! + } + + type TextAttributes { + bold: Boolean + text: String + } +`; diff --git a/federation-integration-testsuite-js/src/fixtures/index.ts b/federation-integration-testsuite-js/src/fixtures/index.ts new file mode 100644 index 000000000..e051f45b7 --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/index.ts @@ -0,0 +1,33 @@ +import * as accounts from './accounts'; +import * as books from './books'; +import * as documents from './documents'; +import * as inventory from './inventory'; +import * as product from './product'; +import * as reviews from './reviews'; + +export { + accounts, + books, + documents, + inventory, + product, + reviews, +}; + +export const fixtures = [ + accounts, + books, + documents, + inventory, + product, + reviews, +]; + +export const fixtureNames = [ + accounts.name, + product.name, + inventory.name, + reviews.name, + books.name, + documents.name, +]; diff --git a/federation-integration-testsuite-js/src/fixtures/inventory.ts b/federation-integration-testsuite-js/src/fixtures/inventory.ts new file mode 100644 index 000000000..603e1bc17 --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/inventory.ts @@ -0,0 +1,62 @@ +import gql from 'graphql-tag'; +import { GraphQLResolverMap } from 'apollo-graphql'; + +export const name = 'inventory'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + extend interface Product { + inStock: Boolean + } + + extend type Furniture implements Product @key(fields: "sku") { + sku: String! @external + inStock: Boolean + isHeavy: Boolean + } + + extend type Book implements Product @key(fields: "isbn") { + isbn: String! @external + inStock: Boolean + isCheckedOut: Boolean + } + + extend type UserMetadata { + description: String @external + } + + extend type User @key(fields: "id") { + id: ID! @external + metadata: [UserMetadata] @external + goodDescription: Boolean @requires(fields: "metadata { description }") + } +`; + +const inventory = [ + { sku: 'TABLE1', inStock: true, isHeavy: false }, + { sku: 'COUCH1', inStock: false, isHeavy: true }, + { sku: 'CHAIR1', inStock: true, isHeavy: false }, + { isbn: '0262510871', inStock: true, isCheckedOut: true }, + { isbn: '0136291554', inStock: false, isCheckedOut: false }, + { isbn: '0201633612', inStock: true, isCheckedOut: false }, +]; + +export const resolvers: GraphQLResolverMap = { + Furniture: { + __resolveReference(object) { + return inventory.find(product => product.sku === object.sku); + }, + }, + Book: { + __resolveReference(object) { + return inventory.find(product => product.isbn === object.isbn); + }, + }, + User: { + goodDescription(object) { + return object.metadata[0].description === '2'; + }, + }, +}; diff --git a/federation-integration-testsuite-js/src/fixtures/product.ts b/federation-integration-testsuite-js/src/fixtures/product.ts new file mode 100644 index 000000000..344222eb4 --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/product.ts @@ -0,0 +1,247 @@ +import gql from 'graphql-tag'; +import { GraphQLResolverMap } from 'apollo-graphql'; + +export const name = 'product'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + extend type Query { + product(upc: String!): Product + vehicle(id: String!): Vehicle + topProducts(first: Int = 5): [Product] + topCars(first: Int = 5): [Car] + } + + type Ikea { + asile: Int + } + + type Amazon { + referrer: String + } + + union Brand = Ikea | Amazon + + interface Product { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + } + + interface ProductDetails { + country: String + } + + type ProductDetailsFurniture implements ProductDetails { + country: String + color: String + } + + type ProductDetailsBook implements ProductDetails { + country: String + pages: Int + } + + type Furniture implements Product @key(fields: "upc") @key(fields: "sku") { + upc: String! + sku: String! + name: String + price: String + brand: Brand + metadata: [MetadataOrError] + details: ProductDetailsFurniture + } + + extend type Book implements Product @key(fields: "isbn") { + isbn: String! @external + title: String @external + year: Int @external + upc: String! + sku: String! + name(delimeter: String = " "): String @requires(fields: "title year") + price: String + details: ProductDetailsBook + } + + interface Vehicle { + id: String! + description: String + price: String + } + + type Car implements Vehicle @key(fields: "id") { + id: String! + description: String + price: String + } + + type Van implements Vehicle @key(fields: "id") { + id: String! + description: String + price: String + } + + union Thing = Car | Ikea + + extend type User @key(fields: "id") { + id: ID! @external + vehicle: Vehicle + thing: Thing + } + + # Value type + type KeyValue { + key: String! + value: String! + } + + # Value type + type Error { + code: Int + message: String + } + + # Value type + union MetadataOrError = KeyValue | Error +`; + +const products = [ + { + __typename: 'Furniture', + upc: '1', + sku: 'TABLE1', + name: 'Table', + price: 899, + brand: { + __typename: 'Ikea', + asile: 10, + }, + metadata: [{ key: 'Condition', value: 'excellent' }], + }, + { + __typename: 'Furniture', + upc: '2', + sku: 'COUCH1', + name: 'Couch', + price: 1299, + brand: { + __typename: 'Amazon', + referrer: 'https://canopy.co', + }, + metadata: [{ key: 'Condition', value: 'used' }], + }, + { + __typename: 'Furniture', + upc: '3', + sku: 'CHAIR1', + name: 'Chair', + price: 54, + brand: { + __typename: 'Ikea', + asile: 10, + }, + metadata: [{ key: 'Condition', value: 'like new' }], + }, + { __typename: 'Book', isbn: '0262510871', price: 39 }, + { __typename: 'Book', isbn: '0136291554', price: 29 }, + { __typename: 'Book', isbn: '0201633612', price: 49 }, + { __typename: 'Book', isbn: '1234567890', price: 59 }, + { __typename: 'Book', isbn: '404404404', price: 0 }, + { __typename: 'Book', isbn: '0987654321', price: 29 }, +]; + +const vehicles = [ + { + __typename: 'Car', + id: '1', + description: 'Humble Toyota', + price: 9990, + }, + { + __typename: 'Car', + id: '2', + description: 'Awesome Tesla', + price: 12990, + }, + { + __typename: 'Van', + id: '3', + description: 'Just a van...', + price: 15990, + }, +]; + +export const resolvers: GraphQLResolverMap = { + Furniture: { + __resolveReference(object) { + return products.find( + product => product.upc === object.upc || product.sku === object.sku, + ); + }, + }, + Book: { + __resolveReference(object) { + if (object.isbn) { + const fetchedObject = products.find( + product => product.isbn === object.isbn, + ); + if (fetchedObject) { + return { ...object, ...fetchedObject }; + } + } + return object; + }, + name(object, { delimeter }) { + return `${object.title}${delimeter}(${object.year})`; + }, + upc(object) { + return object.isbn; + }, + sku(object) { + return object.isbn; + }, + }, + Car: { + __resolveReference(object) { + return vehicles.find(vehicles => vehicles.id === object.id); + }, + }, + Van: { + __resolveReference(object) { + return vehicles.find(vehicles => vehicles.id === object.id); + }, + }, + Thing: { + __resolveType(object) { + return 'id' in object ? 'Car' : 'Ikea'; + }, + }, + User: { + vehicle(user) { + return vehicles.find(vehicles => vehicles.id === user.id); + }, + thing(user) { + return vehicles.find(vehicles => vehicles.id === user.id); + }, + }, + Query: { + product(_, args) { + return products.find(product => product.upc === args.upc); + }, + vehicle(_, args) { + return vehicles.find(vehicles => vehicles.id === args.id); + }, + topProducts(_, args) { + return products.slice(0, args.first); + }, + }, + MetadataOrError: { + __resolveType(object) { + return 'key' in object ? 'KeyValue' : 'Error'; + }, + }, +}; diff --git a/federation-integration-testsuite-js/src/fixtures/reviews.ts b/federation-integration-testsuite-js/src/fixtures/reviews.ts new file mode 100644 index 000000000..7660773bc --- /dev/null +++ b/federation-integration-testsuite-js/src/fixtures/reviews.ts @@ -0,0 +1,241 @@ +import { GraphQLResolverMap } from 'apollo-graphql'; +import gql from 'graphql-tag'; + +export const name = 'reviews'; +export const url = `https://${name}.api.com`; +export const typeDefs = gql` + directive @stream on FIELD + directive @transform(from: String!) on FIELD + + extend type Query { + topReviews(first: Int = 5): [Review] + } + + type Review @key(fields: "id") { + id: ID! + body(format: Boolean = false): String + author: User @provides(fields: "username") + product: Product + metadata: [MetadataOrError] + } + + input UpdateReviewInput { + id: ID! + body: String + } + + extend type UserMetadata { + address: String @external + } + + extend type User @key(fields: "id") { + id: ID! @external + username: String @external + reviews: [Review] + numberOfReviews: Int! + metadata: [UserMetadata] @external + goodAddress: Boolean @requires(fields: "metadata { address }") + } + + extend interface Product { + reviews: [Review] + } + + extend type Furniture implements Product @key(fields: "upc") { + upc: String! @external + reviews: [Review] + } + + extend type Book implements Product @key(fields: "isbn") { + isbn: String! @external + reviews: [Review] + similarBooks: [Book]! @external + relatedReviews: [Review!]! @requires(fields: "similarBooks { isbn }") + } + + extend interface Vehicle { + retailPrice: String + } + + extend type Car implements Vehicle @key(fields: "id") { + id: String! @external + price: String @external + retailPrice: String @requires(fields: "price") + } + + extend type Van implements Vehicle @key(fields: "id") { + id: String! @external + price: String @external + retailPrice: String @requires(fields: "price") + } + + extend type Mutation { + reviewProduct(upc: String!, body: String!): Product + updateReview(review: UpdateReviewInput!): Review + deleteReview(id: ID!): Boolean + } + + # Value type + type KeyValue { + key: String! + value: String! + } + + # Value type + type Error { + code: Int + message: String + } + + # Value type + union MetadataOrError = KeyValue | Error +`; + +const usernames = [ + { id: '1', username: '@ada' }, + { id: '2', username: '@complete' }, +]; +const reviews = [ + { + id: '1', + authorID: '1', + product: { __typename: 'Furniture', upc: '1' }, + body: 'Love it!', + metadata: [{ code: 418, message: "I'm a teapot" }], + }, + { + id: '2', + authorID: '1', + product: { __typename: 'Furniture', upc: '2' }, + body: 'Too expensive.', + }, + { + id: '3', + authorID: '2', + product: { __typename: 'Furniture', upc: '3' }, + body: 'Could be better.', + }, + { + id: '4', + authorID: '2', + product: { __typename: 'Furniture', upc: '1' }, + body: 'Prefer something else.', + }, + { + id: '4', + authorID: '2', + product: { __typename: 'Book', isbn: '0262510871' }, + body: 'Wish I had read this before.', + }, + { + id: '5', + authorID: '2', + product: { __typename: 'Book', isbn: '0136291554' }, + body: 'A bit outdated.', + metadata: [{ key: 'likes', value: '5' }], + }, + { + id: '6', + authorID: '1', + product: { __typename: 'Book', isbn: '0201633612' }, + body: 'A classic.', + }, +]; + +export const resolvers: GraphQLResolverMap = { + Query: { + review(_, args) { + return { id: args.id }; + }, + topReviews(_, args) { + return reviews.slice(0, args.first); + }, + }, + Mutation: { + reviewProduct(_, { upc, body }) { + const id = `${Number(reviews[reviews.length - 1].id) + 1}`; + reviews.push({ + id, + authorID: '1', + product: { __typename: 'Furniture', upc }, + body, + }); + return { upc, __typename: 'Furniture' }; + }, + updateReview(_, { review: { id }, review: updatedReview }) { + let review = reviews.find(review => review.id === id); + + if (!review) { + return null; + } + + review = { + ...review, + ...updatedReview, + }; + + return review; + }, + deleteReview(_, { id }) { + const deleted = reviews.splice( + reviews.findIndex(review => review.id === id), + 1, + ); + return Boolean(deleted); + }, + }, + Review: { + author(review) { + return { __typename: 'User', id: review.authorID }; + }, + }, + User: { + reviews(user) { + return reviews.filter(review => review.authorID === user.id); + }, + numberOfReviews(user) { + return reviews.filter(review => review.authorID === user.id).length; + }, + username(user) { + const found = usernames.find(username => username.id === user.id); + return found ? found.username : null; + }, + goodAddress(object) { + return object.metadata[0].address === '1'; + }, + }, + Furniture: { + reviews(product) { + return reviews.filter(review => review.product.upc === product.upc); + }, + }, + Book: { + reviews(product) { + return reviews.filter(review => review.product.isbn === product.isbn); + }, + relatedReviews(book) { + return book.similarBooks + ? book.similarBooks + .map(({ isbn }: any) => + reviews.filter(review => review.product.isbn === isbn), + ) + .flat() + : []; + }, + }, + Car: { + retailPrice(car) { + return car.price; + }, + }, + Van: { + retailPrice(van) { + return van.price; + }, + }, + MetadataOrError: { + __resolveType(object) { + return 'key' in object ? 'KeyValue' : 'Error'; + }, + }, +}; diff --git a/federation-integration-testsuite-js/src/index.ts b/federation-integration-testsuite-js/src/index.ts new file mode 100644 index 000000000..995b5bc6e --- /dev/null +++ b/federation-integration-testsuite-js/src/index.ts @@ -0,0 +1 @@ +export * from './fixtures'; diff --git a/federation-integration-testsuite-js/tsconfig.json b/federation-integration-testsuite-js/tsconfig.json new file mode 100644 index 000000000..f23c74d80 --- /dev/null +++ b/federation-integration-testsuite-js/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "..//tsconfig.base", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "noImplicitAny": false, + "strictNullChecks": false, + "types": ["node", "jest"] + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__"], + "references": [] +} diff --git a/federation-js/.gitignore b/federation-js/.gitignore new file mode 100644 index 000000000..93d3f513b --- /dev/null +++ b/federation-js/.gitignore @@ -0,0 +1,2 @@ +# JS: Ignore the compiled output. +dist/ diff --git a/federation-js/.npmignore b/federation-js/.npmignore new file mode 100644 index 000000000..a165046d3 --- /dev/null +++ b/federation-js/.npmignore @@ -0,0 +1,6 @@ +* +!src/**/* +!dist/**/* +dist/**/*.test.* +!package.json +!README.md diff --git a/federation-js/CHANGELOG.md b/federation-js/CHANGELOG.md new file mode 100644 index 000000000..9818d4ab0 --- /dev/null +++ b/federation-js/CHANGELOG.md @@ -0,0 +1,177 @@ +# CHANGELOG for `@apollo/federation` + +## vNEXT + +> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. + +- _Nothing yet! Stay tuned!_ + +## v0.20.0 + +- __FIX__: CSDL complex `@key`s shouldn't result in an unparseable document [PR #4490](https://github.com/apollographql/apollo-server/pull/4490) +- __FIX__: Value type validations - restrict unions, scalars, enums [PR #4496](https://github.com/apollographql/apollo-server/pull/4496) +- __FIX__: Composition - aggregate interfaces for types and interfaces in composed schema [PR #4497](https://github.com/apollographql/apollo-server/pull/4497) +- __FIX__: Create new `@key` validations to prevent invalid compositions [PR #4498](https://github.com/apollographql/apollo-server/pull/4498) +- CSDL: make `fields` directive args parseable [PR #4489](https://github.com/apollographql/apollo-server/pull/4489) + +## v0.19.1 + +- Include new directive definitions in CSDL [PR #4452](https://github.com/apollographql/apollo-server/pull/4452) + +## v0.19.0 + +- New federation composition format. Capture federation metadata in SDL [PR #4405](https://github.com/apollographql/apollo-server/pull/4405) + +## v0.18.1 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.18.0 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.17.0 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.11 + +- Reinstate typings for `make-fetch-happen` at the `apollo-gateway` project level (and now, additionally, `apollo-server-plugin-operation-registry`) [PR #4333](https://github.com/apollographql/apollo-server/pull/4333) + +## 0.16.10 + +- The default branch of the repository has been changed to `main`. As this changed a number of references in the repository's `package.json` and `README.md` files (e.g., for badges, links, etc.), this necessitates a release to publish those changes to npm. [PR #4302](https://github.com/apollographql/apollo-server/pull/4302) +- __BREAKING__: Move federation metadata from custom objects on schema nodes over to the `extensions` field on schema nodes which are intended for metadata. This is a breaking change because it narrows the `graphql` peer dependency from `^14.0.2` to `^14.5.0` which is when [`extensions` were introduced](https://github.com/graphql/graphql-js/pull/2097) for all Type System objects. [PR #4302](https://github.com/apollographql/apollo-server/pull/4313) + +## 0.16.9 + +- Handle `@external` validation edge case for interface implementors [#4284](https://github.com/apollographql/apollo-server/pull/4284) + +## 0.16.7 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.6 + +- In-house `Maybe` type which was previously imported from `graphql` and has been moved in `v15.1.0`. [#4230](https://github.com/apollographql/apollo-server/pull/4230) +- Remove remaining common primitives from SDL during composition. This is a follow up to [#4209](https://github.com/apollographql/apollo-server/pull/4209), and additionally removes directives which are included in a schema by default (`@skip`, `@include`, `@deprecated`, and `@specifiedBy`) [#4228](https://github.com/apollographql/apollo-server/pull/4209) + +## v0.16.5 + +- Remove federation primitives from SDL during composition. This allows for services to report their *full* SDL from the `{ _service { sdl } }` query as opposed to the previously limited SDL without federation definitions. [#4209](https://github.com/apollographql/apollo-server/pull/4209) + +## v0.16.4 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.3 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.2 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.1 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.16.0 + +- No changes. This package was major versioned to maintain lockstep versioning with @apollo/gateway. + +## v0.15.1 + +- Export `defaultRootOperationNameLookup` and `normalizeTypeDefs`; needed by `@apollo/gateway` to normalize root operation types when reporting to Apollo Graph Manager. [#4071](https://github.com/apollographql/apollo-server/pull/4071) + +## v0.15.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e37384a49b2bf474eed0de3e9f4a1bebaeee64c7) + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.14.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b898396e9fcd3b9092b168f9aac8466ca186fa6b) + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.14.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/71a3863f59f4ab2c9052c316479d94c6708c4309) + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.13.2 + +- Only changes in the similarly versioned `@apollo/gateway` package. + +## v0.12.1 + +- Fix `v0.12.0` regression: Preserve the `@deprecated` type-system directive as a special case when removing type system directives during composition, resolving an unintentional breaking change introduced by [#3736](https://github.com/apollographql/apollo-server/pull/3736). [#3792](https://github.com/apollographql/apollo-server/pull/3792) + +## v0.12.0 + +- Strip all Type System Directives during composition [#3736](https://github.com/apollographql/apollo-server/pull/3736) +- Prepare for changes in upcoming `graphql@15` release. [#3712](https://github.com/apollographql/apollo-server/pull/3712) + +## v0.11.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/2a4c654986a158aaccf947ee56a4bfc48a3173c7) + +- Ignore TypeSystemDirectiveLocations during composition [#3536](https://github.com/apollographql/apollo-server/pull/3536) + +## v0.11.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/93002737d53dd9a50b473ab9cef14849b3e539aa) + +- Begin supporting executable directives in federation [#3464](https://github.com/apollographql/apollo-server/pull/3464) + +## v0.10.3 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/3cdde1b7a71ace6411fbacf82a1a61bf737444a6) + +- Remove `apollo-env` dependency to eliminate circular dependency between the two packages. This circular dependency makes the tooling repo unpublishable when `apollo-env` requires a version bump. [#3463](https://github.com/apollographql/apollo-server/pull/3463) + +## v0.10.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/aa200ce24b834320fc79d2605dac340b37d3e434) + +- Use reference-equality when omitting validation rules during composition. [#3338](https://github.com/apollographql/apollo-server/pull/3338) + +## v0.10.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/6100fb5e0797cd1f578ded7cb77b60fac47e58e3) + +- Remove federation directives from composed schema [#3272](https://github.com/apollographql/apollo-server/pull/3272) +- Do not remove Query/Mutation/Subscription types when schema is included if schema references those types [#3260](https://github.com/apollographql/apollo-server/pull/3260) + +## v0.9.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/029c8dca3af812ee70589cdb6de749df3d2843d8) + +- Fix value type behavior within composition and execution [#3182](https://github.com/apollographql/apollo-server/pull/2922) + +## v0.6.8 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5974b2ce405a06bc331230400b9073f6381738d3) + +- Support __typenames if defined by an incoming operation [#2922](https://github.com/apollographql/apollo-server/pull/2922) + +## v0.6.7 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/2ea5887acc43461a5539071f4981a5f70e0d0652) + +- Fix bug in externalUnused validation [#2919](https://github.com/apollographql/apollo-server/pull/2919) + +## v0.6.6 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/183de5f112324def375a45c239955e1bf1608fae) + +- Allow specified directives during validation (@deprecated) [#2823](https://github.com/apollographql/apollo-server/pull/2823) + +## v0.6.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/1209839c01b4cac1eb23f42c747296dd9507a8ac) + +- Normalize SDL in a normalization step before validation [#2771](https://github.com/apollographql/apollo-server/pull/2771) diff --git a/federation-js/LICENSE.md b/federation-js/LICENSE.md new file mode 100644 index 000000000..c177b435f --- /dev/null +++ b/federation-js/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2020- Apollo Graph, Inc. +Copyright (c) 2019-2020 Meteor Development Group, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/federation-js/README.md b/federation-js/README.md new file mode 100644 index 000000000..7980e3bdb --- /dev/null +++ b/federation-js/README.md @@ -0,0 +1,40 @@ +# `Apollo Federation Utilities` + +This package provides utilities for creating GraphQL microservices, which can be combined into a single endpoint through tools like [Apollo Gateway](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-gateway). + +For complete documentation, see the [Apollo Federation API reference](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/). + +## Usage + +```js +const { ApolloServer, gql } = require("apollo-server"); +const { buildFederatedSchema } = require("@apollo/federation"); + +const typeDefs = gql` + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + username: String + } +`; + +const resolvers = { + Query: { + me() { + return { id: "1", username: "@ava" } + } + }, + User: { + __resolveReference(user, { fetchUserById }){ + return fetchUserById(user.id) + } + } +}; + +const server = new ApolloServer({ + schema: buildFederatedSchema([{ typeDefs, resolvers }]) +}); +``` diff --git a/federation-js/jest.config.js b/federation-js/jest.config.js new file mode 100644 index 000000000..1fac2072f --- /dev/null +++ b/federation-js/jest.config.js @@ -0,0 +1,19 @@ +const config = require('../jest.config.base'); + +const NODE_MAJOR_VERSION = parseInt( + process.versions.node.split('.', 1)[0], + 10 +); + +const additionalConfig = { + setupFiles: [ + 'core-js/features/array/flat', + 'core-js/features/array/flat-map', + ], + testPathIgnorePatterns: [ + ...config.testPathIgnorePatterns, + ...NODE_MAJOR_VERSION === 6 ? [""] : [] + ] +}; + +module.exports = Object.assign(Object.create(null), config, additionalConfig); diff --git a/federation-js/package.json b/federation-js/package.json new file mode 100644 index 000000000..fb7407f67 --- /dev/null +++ b/federation-js/package.json @@ -0,0 +1,30 @@ +{ + "name": "@apollo/federation", + "version": "0.20.1-alpha.0", + "description": "Apollo Federation Utilities", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/apollographql/federation.git", + "directory": "federation-js/" + }, + "keywords": ["graphql", "federation", "apollo"], + "author": "Apollo ", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "apollo-graphql": "^0.6.0", + "apollo-server-env": "^2.4.5", + "core-js": "^3.4.0", + "lodash.xorby": "^4.7.0" + }, + "peerDependencies": { + "graphql": "^14.5.0 || ^15.0.0" + } +} diff --git a/federation-js/src/__tests__/tsconfig.json b/federation-js/src/__tests__/tsconfig.json new file mode 100644 index 000000000..fcb8707cc --- /dev/null +++ b/federation-js/src/__tests__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.test.base", + "include": ["**/*"], + "references": [ + { "path": "../../" }, + ] +} diff --git a/federation-js/src/composition/__tests__/compose.test.ts b/federation-js/src/composition/__tests__/compose.test.ts new file mode 100644 index 000000000..d458929a2 --- /dev/null +++ b/federation-js/src/composition/__tests__/compose.test.ts @@ -0,0 +1,1391 @@ +import { + GraphQLObjectType, + isSpecifiedDirective, + GraphQLDirective, +} from 'graphql'; +import gql from 'graphql-tag'; +import { composeServices } from '../compose'; +import { + astSerializer, + typeSerializer, + selectionSetSerializer, +} from '../../snapshotSerializers'; +import { normalizeTypeDefs } from '../normalize'; +import { getFederationMetadata } from '../utils'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(typeSerializer); +expect.addSnapshotSerializer(selectionSetSerializer); + +describe('composeServices', () => { + it('should include types from different services', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User { + name: String + email: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('User')).toMatchInlineSnapshot(` + type User { + name: String + email: String! + } + `); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + const user = schema.getType('User') as GraphQLObjectType; + + expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); + expect(getFederationMetadata(user).serviceName).toEqual('serviceB'); + }); + + it("doesn't leave federation directives in the final schema", () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const { schema } = composeServices([serviceA]); + + const directives = schema.getDirectives(); + expect(directives.every(isSpecifiedDirective)); + }); + + describe('basic type extensions', () => { + it('works when extension service is second', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + price: Int! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + price: Int! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); + expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( + 'serviceB', + ); + }); + + it('works when extension service is first', () => { + const serviceA = { + typeDefs: gql` + extend type Product { + price: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceB', + }; + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + price: Int! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); + expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( + 'serviceA', + ); + }); + + it('works with multiple extensions on the same type', () => { + const serviceA = { + typeDefs: gql` + extend type Product { + price: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + extend type Product { + color: String! + } + `, + name: 'serviceC', + }; + + const { schema, errors } = composeServices([ + serviceA, + serviceB, + serviceC, + ]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + price: Int! + color: String! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); + expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( + 'serviceA', + ); + expect(getFederationMetadata(product.getFields()['color']).serviceName).toEqual( + 'serviceC', + ); + }); + + it('allows extensions to overwrite other extension fields', () => { + const serviceA = { + typeDefs: gql` + extend type Product { + price: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + extend type Product { + price: Float! + color: String! + } + `, + name: 'serviceC', + }; + + const { schema, errors } = composeServices([ + serviceA, + serviceB, + serviceC, + ]); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Field "Product.price" can only be defined once.], + ] + `); + expect(schema).toBeDefined(); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + price: Float! + color: String! + } + `); + + expect(getFederationMetadata(product).serviceName).toEqual('serviceB'); + expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( + 'serviceC', + ); + }); + + it('preserves arguments for fields', () => { + const serviceA = { + typeDefs: gql` + enum Curr { + USD + GBP + } + + extend type Product { + price(currency: Curr!): Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + sku: String! + name(type: String): String! + } + `, + name: 'serviceB', + }; + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product { + sku: String! + name(type: String): String! + price(currency: Curr!): Int! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product.getFields()['price'].args[0].name).toEqual('currency'); + }); + + // This is a limitation of extendSchema currently (this is currently a broken test to demonstrate) + it.skip('overwrites field on extension by base type when base type comes second', () => { + const serviceA = { + typeDefs: gql` + extend type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Field "Product.sku" already exists in the schema. It cannot also be defined in this type extension.], + [GraphQLError: Field "Product.name" already exists in the schema. It cannot also be defined in this type extension.], + ] + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + } + `); + expect(getFederationMetadata(product.getFields()['sku']).serviceName).toEqual( + 'serviceB', + ); + expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( + 'serviceB', + ); + }); + + describe('collisions & error handling', () => { + it('handles collisions on type extensions as expected', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + name: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + ] + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + } + `); + expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( + 'serviceB', + ); + }); + + it('reports multiple errors correctly', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! + name: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: [serviceA] Product.sku -> Field "Product.sku" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + ] + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + } + `); + expect(getFederationMetadata(product.getFields()['name']).serviceName).toEqual( + 'serviceB', + ); + }); + + it('handles collisions of base types as expected (newest takes precedence)', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + id: ID! + name: String! + price: Int! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Field "Product.name" can only be defined once.], + [GraphQLError: There can be only one type named "Product".], + ] + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(product).toMatchInlineSnapshot(` + type Product { + id: ID! + name: String! + price: Int! + } + `); + }); + }); + }); + + // Maybe just test conflicts in types + // it("interfaces, unions", () => {}); + + // TODO: _allow_ enum and input extensions, but don't add serviceName + describe('input and enum type extensions', () => { + it('extends input types', () => { + const serviceA = { + typeDefs: gql` + input ProductInput { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend input ProductInput { + color: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(`Array []`); + }); + + it('extends enum types', () => { + const serviceA = { + typeDefs: gql` + enum ProductCategory { + BED + BATH + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend enum ProductCategory { + BEYOND + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toMatchInlineSnapshot(`Array []`); + }); + }); + + describe('interfaces', () => { + // TODO: should there be a validation warning of some sort for this? + it('allows overwriting a type that implements an interface improperly', () => { + const serviceA = { + typeDefs: gql` + interface Item { + id: ID! + } + + type Product implements Item { + id: ID! + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + id: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: [serviceA] Product.id -> Field "Product.id" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + ] + `); + expect(schema).toBeDefined(); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` + type Product implements Item { + id: String! + sku: String! + name: String! + } + `); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); + expect(getFederationMetadata(product.getFields()['id']).serviceName).toEqual( + 'serviceB', + ); + }); + }); + + describe('root type extensions', () => { + it('allows extension of the Query type with no base type definition', () => { + const serviceA = { + typeDefs: gql` + extend type Query { + products: [ID!] + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Query { + people: [ID!] + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` + type Query { + products: [ID!] + people: [ID!] + } + `); + + const query = schema.getQueryType(); + + expect(getFederationMetadata(query).serviceName).toBeUndefined(); + }); + + it('treats root Query type definition as an extension, not base definitions', () => { + const serviceA = { + typeDefs: gql` + type Query { + products: [ID!] + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Query { + people: [ID!] + } + `, + name: 'serviceB', + }; + + const normalizedServices = [serviceA, serviceB].map( + ({ name, typeDefs }) => ({ + name, + typeDefs: normalizeTypeDefs(typeDefs), + }), + ); + const { schema, errors } = composeServices(normalizedServices); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Query')).toMatchInlineSnapshot(` + type Query { + products: [ID!] + people: [ID!] + } + `); + + const query = schema.getType('Query') as GraphQLObjectType; + + expect(getFederationMetadata(query).serviceName).toBeUndefined(); + }); + + it('allows extension of the Mutation type with no base type definition', () => { + const serviceA = { + typeDefs: gql` + extend type Mutation { + login(credentials: Credentials!): String + } + + input Credentials { + username: String! + password: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Mutation { + logout(username: String!): Boolean + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Mutation')).toMatchInlineSnapshot(` + type Mutation { + login(credentials: Credentials!): String + logout(username: String!): Boolean + } + `); + }); + + it('treats root Mutations type definition as an extension, not base definitions', () => { + const serviceA = { + typeDefs: gql` + type Mutation { + login(credentials: Credentials!): String + } + + input Credentials { + username: String! + password: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Mutation { + logout(username: String!): Boolean + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + + expect(schema.getType('Mutation')).toMatchInlineSnapshot(` + type Mutation { + login(credentials: Credentials!): String + logout(username: String!): Boolean + } + `); + }); + + // TODO: not sure what to do here. Haven't looked into it yet :) + it.skip('works with custom root types', () => {}); + }); + + describe('federation directives', () => { + // Directives - allow schema (federation) directives + describe('@external', () => { + it('adds externals map from service to externals for @external fields', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA--FOUND', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB--MISSING', + }; + + const serviceC = { + typeDefs: gql` + extend type Product { + sku: String! @external + upc: String! @external + weight: Int! @requires(fields: "sku upc") + } + `, + name: 'serviceC--found', + }; + + const { schema, errors } = composeServices([ + serviceA, + serviceC, + serviceB, + ]); + + expect(errors).toHaveLength(0); + + const product = schema.getType('Product'); + + expect(getFederationMetadata(product).externals).toMatchInlineSnapshot(` + Object { + "serviceB--MISSING": Array [ + Object { + "field": sku: String! @external, + "parentTypeName": "Product", + "serviceName": "serviceB--MISSING", + }, + ], + "serviceC--found": Array [ + Object { + "field": sku: String! @external, + "parentTypeName": "Product", + "serviceName": "serviceC--found", + }, + Object { + "field": upc: String! @external, + "parentTypeName": "Product", + "serviceName": "serviceC--found", + }, + ], + } + `); + }); + it('does not redefine fields with @external when composing', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(schema).toBeDefined(); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + name: String! + price: Int! + } + `); + expect(getFederationMetadata(product.getFields()['price']).serviceName).toEqual( + 'serviceB', + ); + expect(getFederationMetadata(product).serviceName).toEqual('serviceA'); + }); + }); + + describe('@requires directive', () => { + it('adds @requires information to fields using a simple field set', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect( + getFederationMetadata(product.getFields()['price']).requires, + ).toMatchInlineSnapshot(`sku`); + }); + + it('adds @requires information to fields using a nested field set', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku { id }") { + sku: Sku! + } + + type Sku { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: Sku! @external + price: Float! @requires(fields: "sku { id }") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(getFederationMetadata(product.getFields()['price']).requires) + .toMatchInlineSnapshot(` + sku { + id + } + `); + }); + }); + + // TODO: provides can happen on an extended type as well, add a test case for this + describe('@provides directive', () => { + it('adds @provides information to fields using a simple field set', () => { + const serviceA = { + typeDefs: gql` + type Review { + product: Product @provides(fields: "sku") + } + + extend type Product { + sku: String @external + color: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const review = schema.getType('Review') as GraphQLObjectType; + expect(getFederationMetadata(review.getFields()['product'])).toMatchInlineSnapshot(` + Object { + "belongsToValueType": false, + "provides": sku, + "serviceName": "serviceA", + } + `); + }); + + it('adds @provides information to fields using a nested field set', () => { + const serviceA = { + typeDefs: gql` + type Review { + product: Product @provides(fields: "sku { id }") + } + + extend type Product { + sku: Sku @external + color: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product @key(fields: "sku { id }") { + sku: Sku! + price: Int! @requires(fields: "sku") + } + + type Sku { + id: ID! + value: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const review = schema.getType('Review') as GraphQLObjectType; + expect(getFederationMetadata(review.getFields()['product']).provides) + .toMatchInlineSnapshot(` + sku { + id + } + `); + }); + + it('adds @provides information for object types within list types', () => { + const serviceA = { + typeDefs: gql` + type Review { + products: [Product] @provides(fields: "sku") + } + + extend type Product { + sku: String @external + color: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const review = schema.getType('Review') as GraphQLObjectType; + expect(getFederationMetadata(review.getFields()['products'])) + .toMatchInlineSnapshot(` + Object { + "belongsToValueType": false, + "provides": sku, + "serviceName": "serviceA", + } + `); + }); + + it('adds correct @provides information to fields on value types', () => { + const serviceA = { + typeDefs: gql` + extend type Query { + valueType: ValueType + } + + type ValueType { + id: ID! + user: User! @provides(fields: "id name") + } + + type User @key(fields: "id") { + id: ID! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type ValueType { + id: ID! + user: User! @provides(fields: "id name") + } + + extend type User @key(fields: "id") { + id: ID! @external + name: String! @external + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const valueType = schema.getType('ValueType') as GraphQLObjectType; + const userFieldFederationMetadata = getFederationMetadata(valueType.getFields()['user']); + expect(userFieldFederationMetadata.belongsToValueType).toBe(true); + expect(userFieldFederationMetadata.serviceName).toBe(null); + }); + }); + + describe('@key directive', () => { + it('adds @key information to types using basic string notation', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") @key(fields: "upc") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` + Object { + "serviceA": Array [ + sku, + upc, + ], + } + `); + }); + + it('adds @key information to types using selection set notation', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` + Object { + "serviceA": Array [ + color { + id + value + }, + ], + } + `); + }); + + it('preserves @key information with respect to types across different services', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(getFederationMetadata(product).keys).toMatchInlineSnapshot(` + Object { + "serviceA": Array [ + color { + id + value + }, + ], + "serviceB": Array [ + sku, + ], + } + `); + }); + }); + + describe('@extends directive', () => { + it('treats types with @extends as type extensions', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product @extends @key(fields: "sku") { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const normalizedServices = [serviceA, serviceB].map( + ({ name, typeDefs }) => ({ + name, + typeDefs: normalizeTypeDefs(typeDefs), + }), + ); + const { schema, errors } = composeServices(normalizedServices); + + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + upc: String! + price: Int! + } + `); + }); + + it('treats interfaces with @extends as interface extensions', () => { + const serviceA = { + typeDefs: gql` + interface Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + interface Product @extends @key(fields: "sku") { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const normalizedServices = [serviceA, serviceB].map( + ({ name, typeDefs }) => ({ + name, + typeDefs: normalizeTypeDefs(typeDefs), + }), + ); + const { schema, errors } = composeServices(normalizedServices); + + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product).toMatchInlineSnapshot(` + interface Product { + sku: String! + upc: String! + price: Int! + } + `); + }); + }); + }); + describe('executable directives', () => { + it('keeps executable directives in the schema', () => { + const serviceA = { + typeDefs: gql` + directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + `, + name: 'serviceA', + }; + + const { schema, errors } = composeServices([serviceA]); + + expect(errors).toHaveLength(0); + + const defer = schema.getDirective('defer') as GraphQLDirective; + expect(defer).toMatchInlineSnapshot(`"@defer"`); + }); + it('keeps executable directives in the schema', () => { + const serviceA = { + typeDefs: gql` + directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + directive @stream on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + + expect(errors).toHaveLength(0); + + const defer = schema.getDirective('defer') as GraphQLDirective; + expect(defer).toMatchInlineSnapshot(`"@defer"`); + + const stream = schema.getDirective('stream') as GraphQLDirective; + expect(stream).toMatchInlineSnapshot(`"@stream"`); + }); + }); + + it('extensions field on GraphQLSchema includes serviceList', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + name: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User { + name: String + email: String! + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeServices([serviceA, serviceB]); + expect(errors).toHaveLength(0); + expect(schema).toBeDefined(); + expect(schema.extensions.serviceList).toBeDefined(); + expect(schema.extensions.serviceList).toHaveLength(2); + }); +}); + +// XXX Ignored/unimplemented spec tests +// it("allows extension of custom scalars", () => {}); diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts new file mode 100644 index 000000000..a585a56c6 --- /dev/null +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -0,0 +1,878 @@ +import { composeAndValidate } from '../composeAndValidate'; +import gql from 'graphql-tag'; +import { + GraphQLObjectType, + DocumentNode, + GraphQLScalarType, + specifiedDirectives, + printSchema, +} from 'graphql'; +import { + astSerializer, + typeSerializer, + graphqlErrorSerializer, +} from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(typeSerializer); +expect.addSnapshotSerializer(graphqlErrorSerializer); + +const productsService = { + name: 'Products', + typeDefs: gql` + extend type Query { + topProducts(first: Int): [Product] + } + type Product @key(fields: "upc") { + upc: String! + sku: String! + name: String + price: String + } + `, +}; + +const reviewsService = { + name: 'Reviews', + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + body: String + author: User + product: Product + } + + extend type User @key(fields: "id") { + id: ID! @external + reviews: [Review] + } + extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review] + } + `, +}; + +const accountsService = { + name: 'Accounts', + typeDefs: gql` + extend type Query { + me: User + } + type User @key(fields: "id") { + id: ID! + name: String + username: String + birthDate: String + } + `, +}; + +const inventoryService = { + name: 'Inventory', + typeDefs: gql` + extend type Product @key(fields: "upc") { + upc: String! @external + inStock: Boolean + # quantity: Int + } + `, +}; + +function permutateList(inputArr: T[]) { + let result: T[][] = []; + + function permute(arr: T[], m: T[] = []) { + if (arr.length === 0) { + result.push(m); + } else { + for (let i = 0; i < arr.length; i++) { + let curr = arr.slice(); + let next = curr.splice(i, 1); + permute(curr.slice(), m.concat(next)); + } + } + } + + permute(inputArr); + + return result; +} + +it('composes and validates all (24) permutations without error', () => { + permutateList([ + inventoryService, + reviewsService, + accountsService, + productsService, + ]).map((config) => { + const { errors } = composeAndValidate(config); + + if (errors.length) { + console.error( + `Errors found with composition [${config.map((item) => item.name)}]`, + ); + } + + expect(errors).toHaveLength(0); + }); +}); + +it('errors when a type extension has no base', () => { + const serviceA = { + typeDefs: gql` + schema { + query: MyRoot + } + + type MyRoot { + products: [Product]! + } + + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Location { + id: ID + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(1); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTENSION_WITH_NO_BASE", + "message": "[serviceB] Location -> \`Location\` is an extension type, but \`Location\` is not defined in any service", + }, + ] + `); +}); + +it('treats types with @extends as type extensions', () => { + const serviceA = { + typeDefs: gql` + type Query { + products: [Product]! + } + + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product @extends @key(fields: "sku") { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product).toMatchInlineSnapshot(` + type Product { + sku: String! + upc: String! + price: Int! + } + `); +}); + +it('treats interfaces with @extends as interface extensions', () => { + const serviceA = { + typeDefs: gql` + type Query { + products: [Product]! + } + + interface Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + interface Product @extends @key(fields: "sku") { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const product = schema.getType('Product') as GraphQLObjectType; + expect(product).toMatchInlineSnapshot(` + interface Product { + sku: String! + upc: String! + price: Int! + } + `); +}); + +it('errors on invalid usages of default operation names', () => { + const serviceA = { + typeDefs: gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product @key(fields: "id") { + id: ID! + query: Query + } + + type Query { + invalidUseOfQuery: Boolean + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Query { + validUseOfQuery: Boolean + } + + extend type Product @key(fields: "id") { + id: ID! @external + sku: String + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "ROOT_QUERY_USED", + "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", + }, + ] + `); +}); + +describe('composition of value types', () => { + function getSchemaWithValueType(valueType: DocumentNode) { + const serviceA = { + typeDefs: gql` + ${valueType} + + type Query { + filler: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: valueType, + name: 'serviceB', + }; + + return composeAndValidate([serviceA, serviceB]); + } + + describe('success', () => { + it('scalars', () => { + const { errors, schema } = getSchemaWithValueType( + gql` + scalar Date + `, + ); + expect(errors).toHaveLength(0); + expect(schema.getType('Date')).toMatchInlineSnapshot(`scalar Date`); + }); + + it('unions and object types', () => { + const { errors, schema } = getSchemaWithValueType( + gql` + union CatalogItem = Couch | Mattress + + type Couch { + sku: ID! + material: String! + } + + type Mattress { + sku: ID! + size: String! + } + `, + ); + expect(errors).toHaveLength(0); + expect(schema.getType('CatalogItem')).toMatchInlineSnapshot( + `union CatalogItem = Couch | Mattress`, + ); + expect(schema.getType('Couch')).toMatchInlineSnapshot(` + type Couch { + sku: ID! + material: String! + } + `); + }); + + it('input types', () => { + const { errors, schema } = getSchemaWithValueType(gql` + input NewProductInput { + sku: ID! + type: String + } + `); + expect(errors).toHaveLength(0); + expect(schema.getType('NewProductInput')).toMatchInlineSnapshot(` + input NewProductInput { + sku: ID! + type: String + } + `); + }); + + it('interfaces', () => { + const { errors, schema } = getSchemaWithValueType(gql` + interface Product { + sku: ID! + } + `); + expect(errors).toHaveLength(0); + expect(schema.getType('Product')).toMatchInlineSnapshot(` + interface Product { + sku: ID! + } + `); + }); + + it('enums', () => { + const { errors, schema } = getSchemaWithValueType(gql` + enum CatalogItemEnum { + COUCH + MATTRESS + } + `); + expect(errors).toHaveLength(0); + expect(schema.getType('CatalogItemEnum')).toMatchInlineSnapshot(` + enum CatalogItemEnum { + COUCH + MATTRESS + } + `); + }); + }); + + describe('errors', () => { + it('when used as an entity', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Product { + sku: ID! + color: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Query { + topProducts: [Product] + } + + type Product @key(fields: "sku") { + sku: ID! + color: String! + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_NO_ENTITY", + "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", + } + `); + }); + + it('on field type mismatch', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Product { + sku: ID! + color: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Query { + topProducts: [Product] + } + + type Product { + sku: ID! + color: String + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "message": "[serviceA] Product.color -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.color\` as a String! and String respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", + } + `); + }); + + it('on kind mismatch', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + interface Product { + sku: ID! + color: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Query { + topProducts: [Product] + } + + type Product { + sku: ID! + color: String! + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_KIND_MISMATCH", + "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InterfaceTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", + } + `); + }); + + it('on union types mismatch', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Couch { + sku: ID! + } + + type Mattress { + sku: ID! + } + + union Product = Couch | Mattress + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Query { + topProducts: [Product] + } + + type Couch { + sku: ID! + } + + type Cabinet { + sku: ID! + } + + union Product = Couch | Cabinet + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", + "message": "[serviceA] Product -> The union \`Product\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The types Cabinet, Mattress are mismatched.", + } + `); + }); + }); + + it('composed type implements ALL interfaces that value types implement', () => { + const serviceA = { + typeDefs: gql` + interface Node { + id: ID! + } + + interface Named { + name: String + } + + type Product implements Named & Node { + id: ID! + name: String + } + + type Query { + node(id: ID!): Node + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + interface Node { + id: ID! + } + + type Product implements Node { + id: ID! + name: String + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + interface Named { + name: String + } + + type Product implements Named { + id: ID! + name: String + } + `, + name: 'serviceC', + }; + + const serviceD = { + typeDefs: gql` + type Product { + id: ID! + name: String + } + `, + name: 'serviceD', + }; + + const { schema, errors, composedSdl } = composeAndValidate([ + serviceA, + serviceB, + serviceC, + serviceD, + ]); + + expect(errors).toHaveLength(0); + expect((schema.getType('Product') as GraphQLObjectType).getInterfaces()) + .toHaveLength(2); + + expect(printSchema(schema)).toContain('type Product implements Named & Node'); + expect(composedSdl).toContain('type Product implements Named & Node'); + + }); +}); + +describe('composition of schemas with directives', () => { + /** + * To see which usage sites indicate whether a directive is "executable" or + * merely for use by the type-system ("type-system"), see the GraphQL spec: + * https://graphql.github.io/graphql-spec/June2018/#sec-Type-System.Directives + */ + it('preserves executable and purges type-system directives', () => { + const serviceA = { + typeDefs: gql` + "directives at FIELDs are executable" + directive @audit(risk: Int!) on FIELD + + "directives at FIELD_DEFINITIONs are for the type-system" + directive @transparency(concealment: Int!) on FIELD_DEFINITION + + type EarthConcern { + environmental: String! @transparency(concealment: 5) + } + + extend type Query { + importantDirectives: [EarthConcern!]! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + "directives at FIELDs are executable" + directive @audit(risk: Int!) on FIELD + + "directives at FIELD_DEFINITIONs are for the type-system" + directive @transparency(concealment: Int!) on FIELD_DEFINITION + + "directives at OBJECTs are for the type-system" + directive @experimental on OBJECT + + extend type EarthConcern @experimental { + societal: String! @transparency(concealment: 6) + } + `, + name: 'serviceB', + }; + + const { schema, errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(0); + + const audit = schema.getDirective('audit'); + expect(audit).toMatchInlineSnapshot(`"@audit"`); + + const transparency = schema.getDirective('transparency'); + expect(transparency).toBeUndefined(); + + const type = schema.getType('EarthConcern') as GraphQLObjectType; + + expect(type.astNode).toMatchInlineSnapshot(` + type EarthConcern { + environmental: String! + } + `); + + const fields = type.getFields(); + + expect(fields['environmental'].astNode).toMatchInlineSnapshot( + `environmental: String!`, + ); + + expect(fields['societal'].astNode).toMatchInlineSnapshot( + `societal: String!`, + ); + }); + + it(`doesn't strip the special case @deprecated and @specifiedBy type-system directives`, () => { + const specUrl = 'http://my-spec-url.com'; + const deprecationReason = "Don't remove me please"; + + // Detecting >15.1.0 by the new addition of the `specifiedBy` directive + const isAtLeastGraphqlVersionFifteenPointOne = + specifiedDirectives.length >= 4; + + const serviceA = { + typeDefs: gql` + # This directive needs to be conditionally added depending on the testing + # environment's version of graphql (>= 15.1.0 includes this new directive) + ${ + isAtLeastGraphqlVersionFifteenPointOne + ? `scalar MyScalar @specifiedBy(url: "${specUrl}")` + : '' + } + + type EarthConcern { + environmental: String! + } + + extend type Query { + importantDirectives: [EarthConcern!]! + @deprecated(reason: "${deprecationReason}") + } + `, + name: 'serviceA', + }; + + const { schema, errors } = composeAndValidate([serviceA]); + expect(errors).toHaveLength(0); + + const deprecated = schema.getDirective('deprecated'); + expect(deprecated).toMatchInlineSnapshot(`"@deprecated"`); + + const queryType = schema.getType('Query') as GraphQLObjectType; + const field = queryType.getFields()['importantDirectives']; + + expect(field.isDeprecated).toBe(true); + expect(field.deprecationReason).toEqual(deprecationReason); + + if (isAtLeastGraphqlVersionFifteenPointOne) { + const specifiedBy = schema.getDirective('specifiedBy'); + expect(specifiedBy).toMatchInlineSnapshot(`"@specifiedBy"`); + const customScalar = schema.getType('MyScalar'); + expect((customScalar as GraphQLScalarType).specifiedByUrl).toEqual( + specUrl, + ); + } + }); +}); + +it('composition of full-SDL schemas without any errors', () => { + const serviceA = { + typeDefs: gql` + # Default directives + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + directive @specifiedBy(url: String!) on SCALAR + directive @include( + if: String = "Included when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @skip( + if: String = "Skipped when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + # Federation directives + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + # Custom type system directive (disregarded by gateway, unconcerned with serviceB's implementation) + directive @myTypeSystemDirective on FIELD_DEFINITION + # Custom executable directive (must be implemented in all services, definition must be identical) + directive @myExecutableDirective on FIELD + + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + + schema { + query: RootQuery + mutation: RootMutation + } + + type RootQuery { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + product: Product + } + + type Product @key(fields: "sku") { + sku: String! + price: Float + } + + type RootMutation { + updateProduct: Product + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + # Default directives + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + directive @specifiedBy(url: String!) on SCALAR + directive @include( + if: String = "Included when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @skip( + if: String = "Skipped when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + # Federation directives + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + # Custom type system directive (disregarded by gateway, unconcerned with serviceA's implementation) + directive @myDirective on FIELD_DEFINITION + + # Custom executable directive (must be implemented in all services, definition must be identical) + directive @myExecutableDirective on FIELD + + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + review: Review + } + + type Review @key(fields: "id") { + id: String! + content: String + } + + type Mutation { + createReview: Review + } + `, + name: 'serviceB', + }; + + const { errors } = composeAndValidate([serviceA, serviceB]); + expect(errors).toHaveLength(0); +}); diff --git a/federation-js/src/composition/__tests__/normalize.test.ts b/federation-js/src/composition/__tests__/normalize.test.ts new file mode 100644 index 000000000..6ce8b5367 --- /dev/null +++ b/federation-js/src/composition/__tests__/normalize.test.ts @@ -0,0 +1,410 @@ +import gql from 'graphql-tag'; +import { + defaultRootOperationTypes, + replaceExtendedDefinitionsWithExtensions, + normalizeTypeDefs, + stripCommonPrimitives, +} from '../normalize'; +import { astSerializer } from '../../snapshotSerializers'; +import { specifiedDirectives } from 'graphql'; + +expect.addSnapshotSerializer(astSerializer); + +describe('SDL normalization and its respective parts', () => { + describe('defaultRootOperationTypes', () => { + it('transforms defined root operation types to respective extended default root operation types', () => { + const typeDefs = gql` + schema { + query: RootQuery + mutation: RootMutation + } + + type RootQuery { + product: Product + } + + type Product { + sku: String + } + + type RootMutation { + updateProduct: Product + } + `; + + const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( + typeDefs, + ); + expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` + extend type Query { + product: Product + } + + type Product { + sku: String + } + + extend type Mutation { + updateProduct: Product + } + `); + }); + + it('removes all types using a default root operation type name when a schema definition is provided (root types are defined by the user)', () => { + const typeDefs = gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product { + sku: String + } + + type Query { + removeThisEntireType: String + } + + type Mutation { + removeThisEntireType: String + } + + type Subscription { + removeThisEntireType: String + } + `; + + const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( + typeDefs, + ); + expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` + extend type Query { + product: Product + } + + type Product { + sku: String + } + `); + }); + + it('drops fields that reference an invalid default root operation type name', () => { + const typeDefs = gql` + schema { + query: RootQuery + mutation: RootMutation + } + + type RootQuery { + product: Product + } + + type Query { + removeThisEntireType: String + } + + type RootMutation { + keepThisField: String + removeThisField: Query + } + `; + + const schemaWithDefaultedRootOperationTypes = defaultRootOperationTypes( + typeDefs, + ); + expect(schemaWithDefaultedRootOperationTypes).toMatchInlineSnapshot(` + extend type Query { + product: Product + } + + extend type Mutation { + keepThisField: String + } + `); + }); + }); + + describe('replaceExtendedDefinitionsWithExtensions', () => { + it('transforms the @extends directive into type extensions', () => { + const typeDefs = gql` + type Product @extends @key(fields: "sku") { + sku: String @external + } + `; + + expect(replaceExtendedDefinitionsWithExtensions(typeDefs)) + .toMatchInlineSnapshot(` + extend type Product @key(fields: "sku") { + sku: String @external + } + `); + }); + }); + + describe('stripCommonPrimitives', () => { + it(`removes all common directive definitions`, () => { + // Detecting >15.1.0 by the new addition of the `specifiedBy` directive + const isAtLeastGraphqlVersionFifteenPointOne = + specifiedDirectives.length >= 4; + + const typeDefs = gql` + # Default directives + + # This directive needs to be conditionally added depending on the testing + # environment's version of graphql (>= 15.1.0 includes this new directive) + ${isAtLeastGraphqlVersionFifteenPointOne + ? 'directive @specifiedBy(url: String!) on SCALAR' + : ''} + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + directive @include( + if: String = "Included when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @skip( + if: String = "Skipped when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + # Federation directives + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + type Query { + thing: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`doesn't remove custom directive definitions`, () => { + const typeDefs = gql` + directive @custom on OBJECT + + type Query { + thing: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + directive @custom on OBJECT + + type Query { + thing: String + } + `); + }); + + it(`removes all federation type definitions (scalars, unions, object types)`, () => { + const typeDefs = gql` + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + + type Query { + thing: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`doesn't remove custom scalar, union, or object type definitions`, () => { + const typeDefs = gql` + scalar CustomScalar + + type CustomType { + field: String! + } + + union CustomUnion + + type Query { + thing: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + scalar CustomScalar + + type CustomType { + field: String! + } + + union CustomUnion + + type Query { + thing: String + } + `); + }); + + it(`removes all federation field definitions (_service, _entities)`, () => { + const typeDefs = gql` + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + thing: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Query { + thing: String + } + `); + }); + + it(`removes the Query type altogether if it has no fields left after normalization`, () => { + const typeDefs = gql` + type Query { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + } + + type Custom { + field: String + } + `; + + expect(stripCommonPrimitives(typeDefs)).toMatchInlineSnapshot(` + type Custom { + field: String + } + `); + }); + }); + + describe('normalizeTypeDefs', () => { + it('integration', () => { + // Detecting >15.1.0 by the new addition of the `specifiedBy` directive + const isAtLeastGraphqlVersionFifteenPointOne = + specifiedDirectives.length >= 4; + + const typeDefsToNormalize = gql` + # Default directives + + # This directive needs to be conditionally added depending on the testing + # environment's version of graphql (>= 15.1.0 includes this new directive) + ${isAtLeastGraphqlVersionFifteenPointOne + ? 'directive @specifiedBy(url: String!) on SCALAR' + : ''} + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + directive @include( + if: String = "Included when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @skip( + if: String = "Skipped when true." + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @external on FIELD_DEFINITION + directive @requires(fields: _FieldSet!) on FIELD_DEFINITION + directive @provides(fields: _FieldSet!) on FIELD_DEFINITION + directive @extends on OBJECT | INTERFACE + + scalar _Any + scalar _FieldSet + + union _Entity + + type _Service { + sdl: String + } + + schema { + query: RootQuery + mutation: RootMutation + } + + type RootQuery { + _service: _Service! + _entities(representations: [_Any!]!): [_Entity]! + product: Product + } + + type Product @extends @key(fields: "sku") { + sku: String @external + } + + type RootMutation { + updateProduct: Product + } + `; + + const normalized = normalizeTypeDefs(typeDefsToNormalize); + + expect(normalized).toMatchInlineSnapshot(` + extend type Query { + product: Product + } + + extend type Product @key(fields: "sku") { + sku: String @external + } + + extend type Mutation { + updateProduct: Product + } + `); + }); + + it('should allow schema describing default types', () => { + const typeDefsToNormalize = gql` + schema { + query: Query + mutation: Mutation + } + + type Query { + product: Product + } + + type Product @extends @key(fields: "sku") { + sku: String @external + } + + type Mutation { + updateProduct: Product + } + `; + + const normalized = normalizeTypeDefs(typeDefsToNormalize); + + expect(normalized).toMatchInlineSnapshot(` + extend type Query { + product: Product + } + + extend type Product @key(fields: "sku") { + sku: String @external + } + + extend type Mutation { + updateProduct: Product + } + `); + }); + }); +}); diff --git a/federation-js/src/composition/__tests__/tsconfig.json b/federation-js/src/composition/__tests__/tsconfig.json new file mode 100644 index 000000000..4a90c6e8e --- /dev/null +++ b/federation-js/src/composition/__tests__/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.test.base", + "compilerOptions": { + "strictNullChecks": false + }, + "include": ["**/*"], + "references": [{ "path": "../../../../../" }] +} diff --git a/federation-js/src/composition/__tests__/utils.test.ts b/federation-js/src/composition/__tests__/utils.test.ts new file mode 100644 index 000000000..90164b06a --- /dev/null +++ b/federation-js/src/composition/__tests__/utils.test.ts @@ -0,0 +1,88 @@ +import gql from 'graphql-tag'; +import deepFreeze from 'deep-freeze'; +import { stripExternalFieldsFromTypeDefs } from '../utils'; +import { astSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); + +describe('Composition utility functions', () => { + describe('stripExternalFieldsFromTypeDefs', () => { + it('returns a new DocumentNode with @external fields removed as well as information about the removed fields', () => { + const typeDefs = gql` + type Query { + product: Product + } + + extend type Product @key(fields: "sku") { + sku: String @external + } + + type Mutation { + updateProduct: Product + } + + extend interface Account @key(fields: "id") { + id: ID! @external + } + `; + + const { + typeDefsWithoutExternalFields, + strippedFields, + } = stripExternalFieldsFromTypeDefs(typeDefs, 'serviceA'); + + expect(typeDefsWithoutExternalFields).toMatchInlineSnapshot(` + type Query { + product: Product + } + + extend type Product @key(fields: "sku") + + type Mutation { + updateProduct: Product + } + + extend interface Account @key(fields: "id") + `); + + expect(strippedFields).toMatchInlineSnapshot(` + Array [ + Object { + "field": sku: String @external, + "parentTypeName": "Product", + "serviceName": "serviceA", + }, + Object { + "field": id: ID! @external, + "parentTypeName": "Account", + "serviceName": "serviceA", + }, + ] + `); + }); + + it("doesn't mutate the input DocumentNode", () => { + const typeDefs = gql` + type Query { + product: Product + } + + extend type Product @key(fields: "sku") { + sku: String @external + } + + type Mutation { + updateProduct: Product + } + `; + + deepFreeze(typeDefs); + + // Assert that mutation does, in fact, throw + expect(() => (typeDefs.blah = [])).toThrow(); + expect(() => + stripExternalFieldsFromTypeDefs(typeDefs, 'serviceA'), + ).not.toThrow(); + }); + }); +}); diff --git a/federation-js/src/composition/compose.ts b/federation-js/src/composition/compose.ts new file mode 100644 index 000000000..f5693e6d0 --- /dev/null +++ b/federation-js/src/composition/compose.ts @@ -0,0 +1,659 @@ +import 'apollo-server-env'; +import { + GraphQLSchema, + extendSchema, + Kind, + isTypeDefinitionNode, + isTypeExtensionNode, + GraphQLError, + GraphQLNamedType, + isObjectType, + FieldDefinitionNode, + InputValueDefinitionNode, + DocumentNode, + GraphQLObjectType, + specifiedDirectives, + TypeDefinitionNode, + DirectiveDefinitionNode, + TypeExtensionNode, + ObjectTypeDefinitionNode, + NamedTypeNode, +} from 'graphql'; +import { transformSchema } from 'apollo-graphql'; +import federationDirectives from '../directives'; +import { + findDirectivesOnTypeOrField, + isStringValueNode, + parseSelections, + mapFieldNamesToServiceName, + stripExternalFieldsFromTypeDefs, + typeNodesAreEquivalent, + mapValues, + isFederationDirective, + executableDirectiveLocations, + stripTypeSystemDirectivesFromTypeDefs, + defaultRootOperationNameLookup, + getFederationMetadata, +} from './utils'; +import { + ServiceDefinition, + ExternalFieldDefinition, + ServiceNameToKeyDirectivesMap, + FederationType, + FederationField, + FederationDirective, +} from './types'; +import { validateSDL } from 'graphql/validation/validate'; +import { compositionRules } from './rules'; + +const EmptyQueryDefinition = { + kind: Kind.OBJECT_TYPE_DEFINITION, + name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.query }, + fields: [], + serviceName: null, +}; +const EmptyMutationDefinition = { + kind: Kind.OBJECT_TYPE_DEFINITION, + name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.mutation }, + fields: [], + serviceName: null, +}; + +// Map of all type definitions to eventually be passed to extendSchema +interface TypeDefinitionsMap { + [name: string]: TypeDefinitionNode[]; +} +// Map of all type extensions to eventually be passed to extendSchema +interface TypeExtensionsMap { + [name: string]: TypeExtensionNode[]; +} + +// Map of all directive definitions to eventually be passed to extendSchema +interface DirectiveDefinitionsMap { + [name: string]: { [serviceName: string]: DirectiveDefinitionNode }; +} + +/** + * A map of base types to their owning service. Used by query planner to direct traffic. + * This contains the base type's "owner". Any fields that extend this type in another service + * are listed under "extensionFieldsToOwningServiceMap". extensionFieldsToOwningServiceMap are in the format { myField: my-service-name } + * + * Example resulting typeToServiceMap shape: + * + * const typeToServiceMap = { + * Product: { + * serviceName: "ProductService", + * extensionFieldsToOwningServiceMap: { + * reviews: "ReviewService", // Product.reviews comes from the ReviewService + * dimensions: "ShippingService", + * weight: "ShippingService" + * } + * } + * } + */ +interface TypeToServiceMap { + [typeName: string]: { + owningService?: string; + extensionFieldsToOwningServiceMap: { [fieldName: string]: string }; + }; +} + +/* + * Map of types to their key directives (maintains association to their services) + * + * Example resulting KeyDirectivesMap shape: + * + * const keyDirectives = { + * Product: { + * serviceA: ["sku", "upc"] + * serviceB: ["color {id value}"] // Selection node simplified for readability + * } + * } + */ +export interface KeyDirectivesMap { + [typeName: string]: ServiceNameToKeyDirectivesMap; +} + +/** + * A set of type names that have been determined to be a value type, a type + * shared across at least 2 services. + */ +type ValueTypes = Set; + +export type ComposedGraphQLSchema = GraphQLSchema & { + extensions: { serviceList: ServiceDefinition[] } +}; +/** + * Loop over each service and process its typeDefs (`definitions`) + * - build up typeToServiceMap + * - push individual definitions onto either typeDefinitionsMap or typeExtensionsMap + */ +export function buildMapsFromServiceList(serviceList: ServiceDefinition[]) { + const typeDefinitionsMap: TypeDefinitionsMap = Object.create(null); + const typeExtensionsMap: TypeExtensionsMap = Object.create(null); + const directiveDefinitionsMap: DirectiveDefinitionsMap = Object.create(null); + const typeToServiceMap: TypeToServiceMap = Object.create(null); + const externalFields: ExternalFieldDefinition[] = []; + const keyDirectivesMap: KeyDirectivesMap = Object.create(null); + const valueTypes: ValueTypes = new Set(); + + for (const { typeDefs, name: serviceName } of serviceList) { + // Build a new SDL with @external fields removed, as well as information about + // the fields that were removed. + const { + typeDefsWithoutExternalFields, + strippedFields, + } = stripExternalFieldsFromTypeDefs(typeDefs, serviceName); + + externalFields.push(...strippedFields); + + // Type system directives from downstream services are not a concern of the + // gateway, but rather the services on which the fields live which serve + // those types. In other words, its up to an implementing service to + // act on such directives, not the gateway. + const typeDefsWithoutTypeSystemDirectives = + stripTypeSystemDirectivesFromTypeDefs(typeDefsWithoutExternalFields); + + for (const definition of typeDefsWithoutTypeSystemDirectives.definitions) { + if ( + definition.kind === Kind.OBJECT_TYPE_DEFINITION || + definition.kind === Kind.OBJECT_TYPE_EXTENSION + ) { + const typeName = definition.name.value; + + for (const keyDirective of findDirectivesOnTypeOrField( + definition, + 'key', + )) { + if ( + keyDirective.arguments && + isStringValueNode(keyDirective.arguments[0].value) + ) { + // Initialize the entry for this type if necessary + keyDirectivesMap[typeName] = keyDirectivesMap[typeName] || {}; + // Initialize the entry for this service if necessary + keyDirectivesMap[typeName][serviceName] = + keyDirectivesMap[typeName][serviceName] || []; + // Add @key metadata to the array + keyDirectivesMap[typeName][serviceName].push( + parseSelections(keyDirective.arguments[0].value.value), + ); + } + } + } + + if (isTypeDefinitionNode(definition)) { + const typeName = definition.name.value; + /** + * This type is a base definition (not an extension). If this type is already in the typeToServiceMap, then + * 1. It was declared by a previous service, but this newer one takes precedence, or... + * 2. It was extended by a service before declared + */ + if (!typeToServiceMap[typeName]) { + typeToServiceMap[typeName] = { + extensionFieldsToOwningServiceMap: Object.create(null), + }; + } + + typeToServiceMap[typeName].owningService = serviceName; + + /** + * If this type already exists in the definitions map, push this definition to the array (newer defs + * take precedence). If the types are determined to be identical, add the type name + * to the valueTypes Set. + * + * If not, create the definitions array and add it to the typeDefinitionsMap. + */ + if (typeDefinitionsMap[typeName]) { + const isValueType = typeNodesAreEquivalent( + typeDefinitionsMap[typeName][ + typeDefinitionsMap[typeName].length - 1 + ], + definition, + ); + + if (isValueType) { + valueTypes.add(typeName); + } + + typeDefinitionsMap[typeName].push({ ...definition, serviceName }); + } else { + typeDefinitionsMap[typeName] = [{ ...definition, serviceName }]; + } + } else if (isTypeExtensionNode(definition)) { + const typeName = definition.name.value; + + /** + * This definition is an extension of an OBJECT type defined in another service. + * TODO: handle extensions of non-object types? + */ + if ( + definition.kind === Kind.OBJECT_TYPE_EXTENSION || + definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION + ) { + if (!definition.fields) break; + const fields = mapFieldNamesToServiceName< + FieldDefinitionNode | InputValueDefinitionNode + >(definition.fields, serviceName); + + /** + * If the type already exists in the typeToServiceMap, add the extended fields. If not, create the object + * and add the extensionFieldsToOwningServiceMap, but don't add a serviceName. That will be added once that service + * definition is processed. + */ + if (typeToServiceMap[typeName]) { + typeToServiceMap[typeName].extensionFieldsToOwningServiceMap = { + ...typeToServiceMap[typeName].extensionFieldsToOwningServiceMap, + ...fields, + }; + } else { + typeToServiceMap[typeName] = { + extensionFieldsToOwningServiceMap: fields, + }; + } + } + + if (definition.kind === Kind.ENUM_TYPE_EXTENSION) { + if (!definition.values) break; + + const values = mapFieldNamesToServiceName( + definition.values, + serviceName, + ); + + if (typeToServiceMap[typeName]) { + typeToServiceMap[typeName].extensionFieldsToOwningServiceMap = { + ...typeToServiceMap[typeName].extensionFieldsToOwningServiceMap, + ...values, + }; + } else { + typeToServiceMap[typeName] = { + extensionFieldsToOwningServiceMap: values, + }; + } + } + + /** + * If an extension for this type already exists in the extensions map, push this extension to the + * array (since a type can be extended by multiple services). If not, create the extensions array + * and add it to the typeExtensionsMap. + */ + if (typeExtensionsMap[typeName]) { + typeExtensionsMap[typeName].push({ ...definition, serviceName }); + } else { + typeExtensionsMap[typeName] = [{ ...definition, serviceName }]; + } + } else if (definition.kind === Kind.DIRECTIVE_DEFINITION) { + const directiveName = definition.name.value; + + // The composed schema should only contain directives and their + // ExecutableDirectiveLocations. This filters out any TypeSystemDirectiveLocations. + // A new DirectiveDefinitionNode with this filtered list will be what is + // added to the schema. + const executableLocations = definition.locations.filter(location => + executableDirectiveLocations.includes(location.value), + ); + + // If none of the directive's locations are executable, we don't need to + // include it in the composed schema at all. + if (executableLocations.length === 0) continue; + + const definitionWithExecutableLocations: DirectiveDefinitionNode = { + ...definition, + locations: executableLocations, + }; + + if (directiveDefinitionsMap[directiveName]) { + directiveDefinitionsMap[directiveName][ + serviceName + ] = definitionWithExecutableLocations; + } else { + directiveDefinitionsMap[directiveName] = { + [serviceName]: definitionWithExecutableLocations, + }; + } + } + } + } + + // Since all Query/Mutation definitions in service schemas are treated as + // extensions, we don't have a Query or Mutation DEFINITION in the definitions + // list. Without a Query/Mutation definition, we can't _extend_ the type. + // extendSchema will complain about this. We can't add an empty + // GraphQLObjectType to the schema constructor, so we add an empty definition + // here. We only add mutation if there is a mutation extension though. + if (!typeDefinitionsMap.Query) + typeDefinitionsMap.Query = [EmptyQueryDefinition]; + if (typeExtensionsMap.Mutation && !typeDefinitionsMap.Mutation) + typeDefinitionsMap.Mutation = [EmptyMutationDefinition]; + + return { + typeToServiceMap, + typeDefinitionsMap, + typeExtensionsMap, + directiveDefinitionsMap, + externalFields, + keyDirectivesMap, + valueTypes, + }; +} + +export function buildSchemaFromDefinitionsAndExtensions({ + typeDefinitionsMap, + typeExtensionsMap, + directiveDefinitionsMap, +}: { + typeDefinitionsMap: TypeDefinitionsMap; + typeExtensionsMap: TypeExtensionsMap; + directiveDefinitionsMap: DirectiveDefinitionsMap; +}) { + let errors: GraphQLError[] | undefined = undefined; + + let schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + + // This interface and predicate is a TS / graphql-js workaround for now while + // we're using a local graphql version < v15. This predicate _could_ be: + // `node is ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode` in the + // future to be more semantic. However this gives us type safety and flexibility + // for now. + interface HasInterfaces { + interfaces?: ObjectTypeDefinitionNode['interfaces']; + } + + function nodeHasInterfaces(node: any): node is HasInterfaces { + return 'interfaces' in node; + } + + // Extend the blank schema with the base type definitions (as an AST node) + const definitionsDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [ + ...Object.values(typeDefinitionsMap).flatMap(typeDefinitions => { + // See if any of our Objects or Interfaces implement any interfaces at all. + // If not, we can return early. + if (!typeDefinitions.some(nodeHasInterfaces)) return typeDefinitions; + + const uniqueInterfaces: Map< + string, + NamedTypeNode + > = (typeDefinitions as HasInterfaces[]).reduce( + (map, objectTypeDef) => { + objectTypeDef.interfaces?.forEach((iface) => + map.set(iface.name.value, iface), + ); + return map; + }, + new Map(), + ); + + // No interfaces, no aggregation - just return what we got. + if (uniqueInterfaces.size === 0) return typeDefinitions; + + const [first, ...rest] = typeDefinitions; + + return [ + ...rest, + { + ...first, + interfaces: Array.from(uniqueInterfaces.values()), + }, + ]; + + }), + ...Object.values(directiveDefinitionsMap).map( + definitions => Object.values(definitions)[0], + ), + ], + }; + + errors = validateSDL(definitionsDocument, schema, compositionRules); + schema = extendSchema(schema, definitionsDocument, { assumeValidSDL: true }); + + // Extend the schema with the extension definitions (as an AST node) + const extensionsDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: Object.values(typeExtensionsMap).flat(), + }; + + errors.push(...validateSDL(extensionsDocument, schema, compositionRules)); + + schema = extendSchema(schema, extensionsDocument, { assumeValidSDL: true }); + + // Remove federation directives from the final schema + schema = new GraphQLSchema({ + ...schema.toConfig(), + directives: [ + ...schema.getDirectives().filter(x => !isFederationDirective(x)), + ], + }); + + return { schema, errors }; +} + +/** + * Using the various information we've collected about the schema, augment the + * `schema` itself with `federation` metadata to the types and fields + */ +export function addFederationMetadataToSchemaNodes({ + schema, + typeToServiceMap, + externalFields, + keyDirectivesMap, + valueTypes, + directiveDefinitionsMap, +}: { + schema: GraphQLSchema; + typeToServiceMap: TypeToServiceMap; + externalFields: ExternalFieldDefinition[]; + keyDirectivesMap: KeyDirectivesMap; + valueTypes: ValueTypes; + directiveDefinitionsMap: DirectiveDefinitionsMap; +}) { + for (const [ + typeName, + { owningService, extensionFieldsToOwningServiceMap }, + ] of Object.entries(typeToServiceMap)) { + const namedType = schema.getType(typeName) as GraphQLNamedType; + if (!namedType) continue; + + // Extend each type in the GraphQLSchema with the serviceName that owns it + // and the key directives that belong to it + const isValueType = valueTypes.has(typeName); + const serviceName = isValueType ? null : owningService; + + const federationMetadata: FederationType = { + ...getFederationMetadata(namedType), + serviceName, + isValueType, + ...(keyDirectivesMap[typeName] && { + keys: keyDirectivesMap[typeName], + }), + } + + namedType.extensions = { + ...namedType.extensions, + federation: federationMetadata, + }; + + // For object types, add metadata for all the @provides directives from its fields + if (isObjectType(namedType)) { + for (const field of Object.values(namedType.getFields())) { + const [providesDirective] = findDirectivesOnTypeOrField( + field.astNode, + 'provides', + ); + + if ( + providesDirective && + providesDirective.arguments && + isStringValueNode(providesDirective.arguments[0].value) + ) { + const fieldFederationMetadata: FederationField = { + ...getFederationMetadata(field), + serviceName, + provides: parseSelections( + providesDirective.arguments[0].value.value, + ), + belongsToValueType: isValueType, + } + + field.extensions = { + ...field.extensions, + federation: fieldFederationMetadata + }; + } + } + } + + /** + * For extension fields, do 2 things: + * 1. Add serviceName metadata to all fields that belong to a type extension + * 2. add metadata from the @requires directive for each field extension + */ + for (const [fieldName, extendingServiceName] of Object.entries( + extensionFieldsToOwningServiceMap, + )) { + // TODO: Why don't we need to check for non-object types here + if (isObjectType(namedType)) { + const field = namedType.getFields()[fieldName]; + + const fieldFederationMetadata: FederationField = { + ...getFederationMetadata(field), + serviceName: extendingServiceName, + } + + field.extensions = { + ...field.extensions, + federation: fieldFederationMetadata, + }; + + const [requiresDirective] = findDirectivesOnTypeOrField( + field.astNode, + 'requires', + ); + + if ( + requiresDirective && + requiresDirective.arguments && + isStringValueNode(requiresDirective.arguments[0].value) + ) { + const fieldFederationMetadata: FederationField = { + ...getFederationMetadata(field), + requires: parseSelections( + requiresDirective.arguments[0].value.value, + ), + } + + field.extensions = { + ...field.extensions, + federation: fieldFederationMetadata, + }; + } + } + } + } + // add externals metadata + for (const field of externalFields) { + const namedType = schema.getType(field.parentTypeName); + if (!namedType) continue; + + const existingMetadata = getFederationMetadata(namedType); + const typeFederationMetadata: FederationType = { + ...existingMetadata, + externals: { + ...existingMetadata?.externals, + [field.serviceName]: [ + ...(existingMetadata?.externals?.[field.serviceName] || []), + field, + ], + }, + }; + + namedType.extensions = { + ...namedType.extensions, + federation: typeFederationMetadata, + }; + } + + // add all definitions of a specific directive for validation later + for (const directiveName of Object.keys(directiveDefinitionsMap)) { + const directive = schema.getDirective(directiveName); + if (!directive) continue; + + const directiveFederationMetadata: FederationDirective = { + ...getFederationMetadata(directive), + directiveDefinitions: directiveDefinitionsMap[directiveName], + } + + directive.extensions = { + ...directive.extensions, + federation: directiveFederationMetadata, + } + } +} + +export function composeServices(services: ServiceDefinition[]) { + const { + typeToServiceMap, + typeDefinitionsMap, + typeExtensionsMap, + directiveDefinitionsMap, + externalFields, + keyDirectivesMap, + valueTypes, + } = buildMapsFromServiceList(services); + + let { schema, errors } = buildSchemaFromDefinitionsAndExtensions({ + typeDefinitionsMap, + typeExtensionsMap, + directiveDefinitionsMap, + }); + + // TODO: We should fix this to take non-default operation root types in + // implementing services into account. + schema = new GraphQLSchema({ + ...schema.toConfig(), + ...mapValues(defaultRootOperationNameLookup, typeName => + typeName + ? (schema.getType(typeName) as GraphQLObjectType) + : undefined, + ), + extensions: { + serviceList: services + } + }); + + // If multiple type definitions and extensions for the same type implement the + // same interface, it will get added to the constructed object multiple times, + // resulting in a schema validation error. We therefore need to remove + // duplicate interfaces from object types manually. + schema = transformSchema(schema, type => { + if (isObjectType(type)) { + const config = type.toConfig(); + return new GraphQLObjectType({ + ...config, + interfaces: Array.from(new Set(config.interfaces)), + }); + } + return undefined; + }); + + addFederationMetadataToSchemaNodes({ + schema, + typeToServiceMap, + externalFields, + keyDirectivesMap, + valueTypes, + directiveDefinitionsMap, + }); + + /** + * At the end, we're left with a full GraphQLSchema that _also_ has `serviceName` fields for every type, + * and every field that was extended. Fields that were _not_ extended (added on the base type by the owner), + * there is no `serviceName`, and we should refer to the type's `serviceName` + */ + return { schema: schema as ComposedGraphQLSchema, errors }; +} diff --git a/federation-js/src/composition/composeAndValidate.ts b/federation-js/src/composition/composeAndValidate.ts new file mode 100644 index 000000000..d53229bf8 --- /dev/null +++ b/federation-js/src/composition/composeAndValidate.ts @@ -0,0 +1,47 @@ +import { composeServices } from './compose'; +import { + validateComposedSchema, + validateServicesBeforeComposition, + validateServicesBeforeNormalization, +} from './validate'; +import { ServiceDefinition } from './types'; +import { normalizeTypeDefs } from './normalize'; +import { printComposedSdl } from '../service/printComposedSdl'; + +export function composeAndValidate(serviceList: ServiceDefinition[]) { + const errors = validateServicesBeforeNormalization(serviceList); + + const normalizedServiceList = serviceList.map(({ name, typeDefs }) => ({ + name, + typeDefs: normalizeTypeDefs(typeDefs), + })); + + // generate errors or warnings of the individual services + errors.push(...validateServicesBeforeComposition(normalizedServiceList)); + + // generate a schema and any errors or warnings + const compositionResult = composeServices(normalizedServiceList); + errors.push(...compositionResult.errors); + + // validate the composed schema based on service information + errors.push( + ...validateComposedSchema({ + schema: compositionResult.schema, + serviceList, + }), + ); + + // We shouldn't try to print the SDL if there were errors during composition + const composedSdl = + errors.length === 0 + ? printComposedSdl(compositionResult.schema, serviceList) + : undefined; + + // TODO remove the warnings array once no longer used by clients + return { + schema: compositionResult.schema, + warnings: [], + errors, + composedSdl, + }; +} diff --git a/federation-js/src/composition/index.ts b/federation-js/src/composition/index.ts new file mode 100644 index 000000000..58c910b8a --- /dev/null +++ b/federation-js/src/composition/index.ts @@ -0,0 +1,6 @@ +export * from './compose'; +export * from './composeAndValidate'; +export * from './types'; +export { compositionRules } from './rules'; +export { normalizeTypeDefs } from './normalize'; +export { defaultRootOperationNameLookup } from './utils'; diff --git a/federation-js/src/composition/normalize.ts b/federation-js/src/composition/normalize.ts new file mode 100644 index 000000000..9f8fcacb2 --- /dev/null +++ b/federation-js/src/composition/normalize.ts @@ -0,0 +1,321 @@ +import { DefaultRootOperationTypeName } from './types'; +import { + DocumentNode, + visit, + ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, + Kind, + InterfaceTypeDefinitionNode, + VisitFn, + specifiedDirectives, +} from 'graphql'; +import { + findDirectivesOnTypeOrField, + defKindToExtKind, + reservedRootFields, + defaultRootOperationNameLookup +} from './utils'; +import federationDirectives from '../directives'; + +export function normalizeTypeDefs(typeDefs: DocumentNode) { + // The order of this is important - `stripCommonPrimitives` must come after + // `defaultRootOperationTypes` because it depends on the `Query` type being named + // its default: `Query`. + return stripCommonPrimitives( + defaultRootOperationTypes( + replaceExtendedDefinitionsWithExtensions(typeDefs), + ), + ); +} + +export function defaultRootOperationTypes( + typeDefs: DocumentNode, +): DocumentNode { + // Array of default root operation names + const defaultRootOperationNames = Object.values( + defaultRootOperationNameLookup, + ); + + // Map of the given root operation type names to their respective default operation + // type names, i.e. {RootQuery: 'Query'} + let rootOperationTypeMap: { + [key: string]: DefaultRootOperationTypeName; + } = Object.create(null); + + let hasSchemaDefinitionOrExtension = false; + visit(typeDefs, { + OperationTypeDefinition(node) { + // If we find at least one root operation type definition, we know the user has + // specified either a schema definition or extension. + hasSchemaDefinitionOrExtension = true; + // Build the map of root operation type name to its respective default + rootOperationTypeMap[node.type.name.value] = + defaultRootOperationNameLookup[node.operation]; + }, + }); + + // In this case, there's no defined schema or schema extension, so we use defaults + if (!hasSchemaDefinitionOrExtension) { + rootOperationTypeMap = { + Query: 'Query', + Mutation: 'Mutation', + Subscription: 'Subscription', + }; + } + + // A conflicting default definition exists when the user provides a schema + // definition, but also defines types that use the default root operation + // names (Query, Mutation, Subscription). Those types need to be removed. + let schemaWithoutConflictingDefaultDefinitions; + if (!hasSchemaDefinitionOrExtension) { + // If no schema definition or extension exists, then there aren't any + // conflicting defaults to worry about. + schemaWithoutConflictingDefaultDefinitions = typeDefs; + } else { + // If the user provides a schema definition or extension, then using default + // root operation names is considered an error for composition. This visit + // drops the invalid type definitions/extensions altogether, as well as + // fields that reference them. + // + // Example: + // + // schema { + // query: RootQuery + // } + // + // type Query { <--- this type definition is invalid (as well as Mutation or Subscription) + // ... + // } + schemaWithoutConflictingDefaultDefinitions = visit(typeDefs, { + ObjectTypeDefinition(node) { + if ( + (defaultRootOperationNames as string[]).includes(node.name.value) && + !rootOperationTypeMap[node.name.value] + ) { + return null; + } + return; + }, + ObjectTypeExtension(node) { + if ( + (defaultRootOperationNames as string[]).includes(node.name.value) && + !rootOperationTypeMap[node.name.value] + ) { + return null; + } + return; + }, + // This visitor handles the case where: + // 1) A schema definition or extension is provided by the user + // 2) A field exists that is of a _default_ root operation type. (Query, Mutation, Subscription) + // + // Example: + // + // schema { + // mutation: RootMutation + // } + // + // type RootMutation { + // updateProduct: Query <--- remove this field altogether + // } + FieldDefinition(node) { + if ( + node.type.kind === Kind.NAMED_TYPE && + (defaultRootOperationNames as string[]).includes(node.type.name.value) + ) { + return null; + } + + if ( + node.type.kind === Kind.NON_NULL_TYPE && + node.type.type.kind === Kind.NAMED_TYPE && + (defaultRootOperationNames as string[]).includes( + node.type.type.name.value, + ) + ) { + return null; + } + return; + }, + }); + } + + const schemaWithDefaultRootTypes = visit( + schemaWithoutConflictingDefaultDefinitions, + { + // Schema definitions and extensions are extraneous since we're transforming + // the root operation types to their defaults. + SchemaDefinition() { + return null; + }, + SchemaExtension() { + return null; + }, + ObjectTypeDefinition(node) { + if ( + node.name.value in rootOperationTypeMap || + (defaultRootOperationNames as string[]).includes(node.name.value) + ) { + return { + ...node, + name: { + ...node.name, + value: rootOperationTypeMap[node.name.value] || node.name.value, + }, + kind: Kind.OBJECT_TYPE_EXTENSION, + }; + } + return; + }, + // schema { + // query: RootQuery + // } + // + // extend type RootQuery { <--- update this to `extend type Query` + // ... + // } + ObjectTypeExtension(node) { + if ( + node.name.value in rootOperationTypeMap || + (defaultRootOperationNames as string[]).includes(node.name.value) + ) { + return { + ...node, + name: { + ...node.name, + value: rootOperationTypeMap[node.name.value] || node.name.value, + }, + }; + } + return; + }, + // Corresponding NamedTypes must also make the name switch, in the case that + // they reference a root operation type that we've transformed + // + // schema { + // query: RootQuery + // mutation: RootMutation + // } + // + // type RootQuery { + // ... + // } + // + // type RootMutation { + // updateProduct: RootQuery <--- rename `RootQuery` to `Query` + // } + NamedType(node) { + if (node.name.value in rootOperationTypeMap) { + return { + ...node, + name: { + ...node.name, + value: rootOperationTypeMap[node.name.value], + }, + }; + } + return; + }, + }, + ); + + return schemaWithDefaultRootTypes; +} + +// type definitions with the @extends directive should be treated +// as type extensions. +export function replaceExtendedDefinitionsWithExtensions( + typeDefs: DocumentNode, +) { + const typeDefsWithExtendedTypesReplaced = visit(typeDefs, { + ObjectTypeDefinition: visitor, + InterfaceTypeDefinition: visitor, + }); + + function visitor( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + ) { + const isExtensionDefinition = + findDirectivesOnTypeOrField(node, 'extends').length > 0; + + if (!isExtensionDefinition) { + return node; + } + + const filteredDirectives = + node.directives && + node.directives.filter(directive => directive.name.value !== 'extends'); + + return { + ...node, + ...(filteredDirectives && { directives: filteredDirectives }), + kind: defKindToExtKind[node.kind], + }; + } + + return typeDefsWithExtendedTypesReplaced; +} + +// For non-ApolloServer libraries that support federation, this allows a +// library to report the entire schema's SDL rather than an awkward, stripped out +// subset of the schema. Generally there's no need to include the federation +// primitives, but in many cases it's more difficult to exclude them. +// +// This removes the following from a GraphQL Document: +// directives: @external, @key, @requires, @provides, @extends, @skip, @include, @deprecated, @specifiedBy +// scalars: _Any, _FieldSet +// union: _Entity +// object type: _Service +// Query fields: _service, _entities +export function stripCommonPrimitives(document: DocumentNode) { + const typeDefinitionVisitor: VisitFn< + any, + ObjectTypeDefinitionNode | ObjectTypeExtensionNode + > = (node) => { + // Remove the `_entities` and `_service` fields from the `Query` type + if (node.name.value === defaultRootOperationNameLookup.query) { + const filteredFieldDefinitions = node.fields?.filter( + (fieldDefinition) => + !reservedRootFields.includes(fieldDefinition.name.value), + ); + + // If the 'Query' type is now empty just remove it + if (!filteredFieldDefinitions || filteredFieldDefinitions.length === 0) { + return null; + } + + return { + ...node, + fields: filteredFieldDefinitions, + }; + } + + // Remove the _Service type from the document + const isFederationType = node.name.value === '_Service'; + return isFederationType ? null : node; + }; + + return visit(document, { + // Remove all common directive definitions from the document + DirectiveDefinition(node) { + const isCommonDirective = [...federationDirectives, ...specifiedDirectives].some( + (directive) => directive.name === node.name.value, + ); + return isCommonDirective ? null : node; + }, + // Remove all federation scalar definitions from the document + ScalarTypeDefinition(node) { + const isFederationScalar = ['_Any', '_FieldSet'].includes( + node.name.value, + ); + return isFederationScalar ? null : node; + }, + // Remove all federation union definitions from the document + UnionTypeDefinition(node) { + const isFederationUnion = node.name.value === "_Entity"; + return isFederationUnion ? null : node; + }, + ObjectTypeDefinition: typeDefinitionVisitor, + ObjectTypeExtension: typeDefinitionVisitor, + }); +} diff --git a/federation-js/src/composition/rules.ts b/federation-js/src/composition/rules.ts new file mode 100644 index 000000000..f4ed37e23 --- /dev/null +++ b/federation-js/src/composition/rules.ts @@ -0,0 +1,59 @@ +import { specifiedSDLRules } from 'graphql/validation/specifiedRules'; + +/** + * Since this module has overlapping names in the two modules (graphql-js and + * our own, local validation rules) which we are importing from, we + * intentionally are very explicit about the suffixes of imported members here, + * so that the intention is clear. + * + * First, we'll import validation rules from graphql-js which we'll omit and + * replace with our own validation rules. As noted above, we'll use aliases + * with 'FromGraphqlJs' suffixes for clarity. + */ + +import { + UniqueDirectivesPerLocationRule as UniqueDirectivesPerLocationRuleFromGraphqlJs, +} from 'graphql'; +import { + UniqueTypeNames as UniqueTypeNamesFromGraphqlJs, +} from 'graphql/validation/rules/UniqueTypeNames'; +import { + UniqueEnumValueNames as UniqueEnumValueNamesFromGraphqlJs, +} from 'graphql/validation/rules/UniqueEnumValueNames'; +import { + PossibleTypeExtensions as PossibleTypeExtensionsFromGraphqlJs, +} from 'graphql/validation/rules/PossibleTypeExtensions'; +import { + UniqueFieldDefinitionNames as UniqueFieldDefinitionNamesFromGraphqlJs, +} from 'graphql/validation/rules/UniqueFieldDefinitionNames'; + +/** + * Then, we'll import our own validation rules to take the place of those that + * we'll be customizing, taking care to alias them all to the same name with + * "FromComposition" suffixes. + */ +import { + UniqueTypeNamesWithFields as UniqueTypeNamesWithFieldsFromComposition, + MatchingEnums as MatchingEnumsFromComposition, + PossibleTypeExtensions as PossibleTypeExtensionsFromComposition, + UniqueFieldDefinitionNames as UniqueFieldDefinitionsNamesFromComposition, + UniqueUnionTypes as UniqueUnionTypesFromComposition, + } from './validate/sdl'; + +const omit = [ + UniqueDirectivesPerLocationRuleFromGraphqlJs, + UniqueTypeNamesFromGraphqlJs, + UniqueEnumValueNamesFromGraphqlJs, + PossibleTypeExtensionsFromGraphqlJs, + UniqueFieldDefinitionNamesFromGraphqlJs, +]; + +export const compositionRules = specifiedSDLRules + .filter(rule => !omit.includes(rule)) + .concat([ + UniqueFieldDefinitionsNamesFromComposition, + UniqueTypeNamesWithFieldsFromComposition, + MatchingEnumsFromComposition, + UniqueUnionTypesFromComposition, + PossibleTypeExtensionsFromComposition, + ]); diff --git a/federation-js/src/composition/types.ts b/federation-js/src/composition/types.ts new file mode 100644 index 000000000..182ed0d5b --- /dev/null +++ b/federation-js/src/composition/types.ts @@ -0,0 +1,102 @@ +import { + SelectionNode, + DocumentNode, + FieldDefinitionNode, + DirectiveDefinitionNode, +} from 'graphql'; + +export type Maybe = null | undefined | T; + +export type ServiceName = string | null; + +export type DefaultRootOperationTypeName = + | 'Query' + | 'Mutation' + | 'Subscription'; + +export interface ExternalFieldDefinition { + field: FieldDefinitionNode; + parentTypeName: string; + serviceName: string; +} + +export interface ServiceNameToKeyDirectivesMap { + [serviceName: string]: ReadonlyArray[]; +} + +export interface FederationType { + serviceName?: ServiceName; + keys?: ServiceNameToKeyDirectivesMap; + externals?: { + [serviceName: string]: ExternalFieldDefinition[]; + }; + isValueType?: boolean; +} + +export interface FederationField { + serviceName?: ServiceName; + requires?: ReadonlyArray; + provides?: ReadonlyArray; + belongsToValueType?: boolean; +} + +export interface FederationDirective { + directiveDefinitions: { + [serviceName: string]: DirectiveDefinitionNode; + } +} + +export interface ServiceDefinition { + typeDefs: DocumentNode; + name: string; + url?: string; +} + +declare module 'graphql/language/ast' { + interface UnionTypeDefinitionNode { + serviceName?: string | null; + } + interface UnionTypeExtensionNode { + serviceName?: string | null; + } + + interface EnumTypeDefinitionNode { + serviceName?: string | null; + } + + interface EnumTypeExtensionNode { + serviceName?: string | null; + } + + interface ScalarTypeDefinitionNode { + serviceName?: string | null; + } + + interface ScalarTypeExtensionNode { + serviceName?: string | null; + } + + interface ObjectTypeDefinitionNode { + serviceName?: string | null; + } + + interface ObjectTypeExtensionNode { + serviceName?: string | null; + } + + interface InterfaceTypeDefinitionNode { + serviceName?: string | null; + } + + interface InterfaceTypeExtensionNode { + serviceName?: string | null; + } + + interface InputObjectTypeDefinitionNode { + serviceName?: string | null; + } + + interface InputObjectTypeExtensionNode { + serviceName?: string | null; + } +} diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts new file mode 100644 index 000000000..2c0a0ea9e --- /dev/null +++ b/federation-js/src/composition/utils.ts @@ -0,0 +1,583 @@ +import 'apollo-server-env'; +import { + InterfaceTypeExtensionNode, + FieldDefinitionNode, + Kind, + StringValueNode, + parse, + OperationDefinitionNode, + NameNode, + DocumentNode, + visit, + ObjectTypeExtensionNode, + DirectiveNode, + GraphQLNamedType, + GraphQLError, + GraphQLSchema, + isObjectType, + GraphQLObjectType, + getNamedType, + GraphQLField, + SelectionNode, + isEqualType, + FieldNode, + TypeDefinitionNode, + InputValueDefinitionNode, + TypeExtensionNode, + BREAK, + print, + ASTNode, + DirectiveDefinitionNode, + GraphQLDirective, + OperationTypeNode, + isDirective, + isNamedType, +} from 'graphql'; +import { + ExternalFieldDefinition, + DefaultRootOperationTypeName, + Maybe, + FederationType, + FederationDirective, + FederationField, +} from './types'; +import federationDirectives from '../directives'; + +export function isStringValueNode(node: any): node is StringValueNode { + return node.kind === Kind.STRING; +} + +// Create a map of { fieldName: serviceName } for each field. +export function mapFieldNamesToServiceName( + fields: ReadonlyArray, + serviceName: string, +) { + return fields.reduce((prev, next) => { + prev[next.name.value] = serviceName; + return prev; + }, Object.create(null)); +} + +export function findDirectivesOnTypeOrField( + node: Maybe, + directiveName: string, +) { + return node && node.directives + ? node.directives.filter( + directive => directive.name.value === directiveName, + ) + : []; +} + +export function stripExternalFieldsFromTypeDefs( + typeDefs: DocumentNode, + serviceName: string, +): { + typeDefsWithoutExternalFields: DocumentNode; + strippedFields: ExternalFieldDefinition[]; +} { + const strippedFields: ExternalFieldDefinition[] = []; + + const typeDefsWithoutExternalFields = visit(typeDefs, { + ObjectTypeExtension: removeExternalFieldsFromExtensionVisitor( + strippedFields, + serviceName, + ), + InterfaceTypeExtension: removeExternalFieldsFromExtensionVisitor( + strippedFields, + serviceName, + ), + }) as DocumentNode; + + return { typeDefsWithoutExternalFields, strippedFields }; +} + +export function stripTypeSystemDirectivesFromTypeDefs(typeDefs: DocumentNode) { + const typeDefsWithoutTypeSystemDirectives = visit(typeDefs, { + Directive(node) { + // The `deprecated` directive is an exceptional case that we want to leave in + if (node.name.value === 'deprecated' || node.name.value === 'specifiedBy') return; + + const isFederationDirective = federationDirectives.some( + ({ name }) => name === node.name.value, + ); + // Returning `null` to a visit will cause it to be removed from the tree. + return isFederationDirective ? undefined : null; + }, + }) as DocumentNode; + + return typeDefsWithoutTypeSystemDirectives; +} + +/** + * Returns a closure that strips fields marked with `@external` and adds them + * to an array. + * @param collector + * @param serviceName + */ +function removeExternalFieldsFromExtensionVisitor< + T extends InterfaceTypeExtensionNode | ObjectTypeExtensionNode +>(collector: ExternalFieldDefinition[], serviceName: string) { + return (node: T) => { + let fields = node.fields; + if (fields) { + fields = fields.filter(field => { + const externalDirectives = findDirectivesOnTypeOrField( + field, + 'external', + ); + + if (externalDirectives.length > 0) { + collector.push({ + field, + parentTypeName: node.name.value, + serviceName, + }); + return false; + } + return true; + }); + } + return { + ...node, + fields, + }; + }; +} + +export function parseSelections(source: string) { + return (parse(`query { ${source} }`) + .definitions[0] as OperationDefinitionNode).selectionSet.selections; +} + +export function hasMatchingFieldInDirectives({ + directives, + fieldNameToMatch, + namedType, +}: { + directives: DirectiveNode[]; + fieldNameToMatch: String; + namedType: GraphQLNamedType; +}) { + return Boolean( + namedType.astNode && + directives + // for each key directive, get the fields arg + .map(keyDirective => + keyDirective.arguments && + isStringValueNode(keyDirective.arguments[0].value) + ? { + typeName: namedType.astNode!.name.value, + keyArgument: keyDirective.arguments[0].value.value, + } + : null, + ) + // filter out any null/undefined args + .filter(isNotNullOrUndefined) + // flatten all selections of the "fields" arg to a list of fields + .flatMap(selection => parseSelections(selection.keyArgument)) + // find a field that matches the @external field + .some( + field => + field.kind === Kind.FIELD && field.name.value === fieldNameToMatch, + ), + ); +} + +export const logServiceAndType = ( + serviceName: string, + typeName: string, + fieldName?: string, +) => `[${serviceName}] ${typeName}${fieldName ? `.${fieldName} -> ` : ' -> '}`; + +export function logDirective(directiveName: string) { + return `[@${directiveName}] -> `; +} + +// TODO: allow passing of the other args here, rather than just message and code +export function errorWithCode( + code: string, + message: string, + nodes?: ReadonlyArray | ASTNode | undefined, +) { + return new GraphQLError( + message, + nodes, + undefined, + undefined, + undefined, + undefined, + { + code, + }, + ); +} + +export function findTypesContainingFieldWithReturnType( + schema: GraphQLSchema, + node: GraphQLField, +): GraphQLObjectType[] { + const returnType = getNamedType(node.type); + if (!isObjectType(returnType)) return []; + + const containingTypes: GraphQLObjectType[] = []; + const types = schema.getTypeMap(); + for (const selectionSetType of Object.values(types)) { + // Only object types have fields + if (!isObjectType(selectionSetType)) continue; + const allFields = selectionSetType.getFields(); + + // only push types that have a field which returns the returnType + Object.values(allFields).forEach(field => { + const fieldReturnType = getNamedType(field.type); + if (fieldReturnType === returnType) { + containingTypes.push(fieldReturnType); + } + }); + } + return containingTypes; +} + +/** + * Used for finding a field on the `schema` that returns `typeToFind` + * + * Used in validation of external directives to find uses of a field in a + * `@provides` on another type. + */ +export function findFieldsThatReturnType({ + schema, + typeToFind, +}: { + schema: GraphQLSchema; + typeToFind: GraphQLNamedType; +}): GraphQLField[] { + if (!isObjectType(typeToFind)) return []; + + const fieldsThatReturnType: GraphQLField[] = []; + const types = schema.getTypeMap(); + + for (const selectionSetType of Object.values(types)) { + // for our purposes, only object types have fields that we care about. + if (!isObjectType(selectionSetType)) continue; + + const fieldsOnNamedType = selectionSetType.getFields(); + + // push fields that have return `typeToFind` + Object.values(fieldsOnNamedType).forEach(field => { + const fieldReturnType = getNamedType(field.type); + if (fieldReturnType === typeToFind) { + fieldsThatReturnType.push(field); + } + }); + } + return fieldsThatReturnType; +} + +/** + * Searches recursively to see if a selection set includes references to + * `typeToFind.fieldToFind`. + * + * Used in validation of external fields to find where/if a field is referenced + * in a nested selection set for `@requires` + * + * For every selection, look at the root of the selection's type. + * 1. If it's the type we're looking for, check its fields. + * Return true if field matches. Skip to step 3 if not + * 2. If it's not the type we're looking for, skip to step 3 + * 3. Get the return type for each subselection and run this function on the subselection. + */ +export function selectionIncludesField({ + selections, + selectionSetType, + typeToFind, + fieldToFind, +}: { + selections: readonly SelectionNode[]; + selectionSetType: GraphQLObjectType; // type which applies to `selections` + typeToFind: GraphQLObjectType; // type where the `@external` lives + fieldToFind: string; +}): boolean { + for (const selection of selections as FieldNode[]) { + const selectionName: string = selection.name.value; + + // if the selected field matches the fieldname we're looking for, + // and its type is correct, we're done. Return true; + if ( + selectionName === fieldToFind && + isEqualType(selectionSetType, typeToFind) + ) + return true; + + // if the field selection has a subselection, check each field recursively + + // check to make sure the parent type contains the field + const typeIncludesField = + selectionName && + Object.keys(selectionSetType.getFields()).includes(selectionName); + if (!selectionName || !typeIncludesField) continue; + + // get the return type of the selection + const returnType = getNamedType( + selectionSetType.getFields()[selectionName].type, + ); + if (!returnType || !isObjectType(returnType)) continue; + const subselections = + selection.selectionSet && selection.selectionSet.selections; + + // using the return type of a given selection and all the subselections, + // recursively search for matching selections. typeToFind and fieldToFind + // stay the same + if (subselections) { + const selectionDoesIncludeField = selectionIncludesField({ + selectionSetType: returnType, + selections: subselections, + typeToFind, + fieldToFind, + }); + if (selectionDoesIncludeField) return true; + } + } + return false; +} + +/** + * Returns true if a @key directive is found on the type node + * + * @param node TypeDefinitionNode | TypeExtensionNode + * @returns boolean + */ +export function isTypeNodeAnEntity( + node: TypeDefinitionNode | TypeExtensionNode, +) { + let isEntity = false; + + visit(node, { + Directive(directive) { + if (directive.name.value === 'key') { + isEntity = true; + return BREAK; + } + }, + }); + + return isEntity; +} + +/** + * Diff two type nodes. This returns an object consisting of useful properties and their differences + * - name: An array of length 0 or 2. If their type names are different, they will be added to the array. + * (['Product', 'Product']) + * - fields: An entry in the fields object can mean two things: + * 1) a field was found on one type, but not the other (fieldName: ['String!']) + * 2) a common field was found, but their types differ (fieldName: ['String!', 'Int!']) + * - kind: An array of length 0 or 2. If their kinds are different, they will be added to the array. + * (['InputObjectTypeDefinition', 'InterfaceTypeDefinition']) + * + * @param firstNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode + * @param secondNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode + */ +export function diffTypeNodes( + firstNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, + secondNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, +) { + const fieldsDiff: { + [fieldName: string]: string[]; + } = Object.create(null); + + const unionTypesDiff: { + [typeName: string]: boolean; + } = Object.create(null); + + const locationsDiff: Set = new Set(); + + const argumentsDiff: { + [argumentName: string]: string[]; + } = Object.create(null); + + const document: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [firstNode, secondNode], + }; + + function fieldVisitor(node: FieldDefinitionNode | InputValueDefinitionNode) { + const fieldName = node.name.value; + + const type = print(node.type); + + if (!fieldsDiff[fieldName]) { + fieldsDiff[fieldName] = [type]; + return; + } + + // If we've seen this field twice and the types are the same, remove this + // field from the diff result + const fieldTypes = fieldsDiff[fieldName]; + if (fieldTypes[0] === type) { + delete fieldsDiff[fieldName]; + } else { + fieldTypes.push(type); + } + } + + visit(document, { + FieldDefinition: fieldVisitor, + InputValueDefinition: fieldVisitor, + UnionTypeDefinition(node) { + if (!node.types) return BREAK; + for (const namedTypeNode of node.types) { + const name = namedTypeNode.name.value; + if (unionTypesDiff[name]) { + delete unionTypesDiff[name]; + } else { + unionTypesDiff[name] = true; + } + } + }, + DirectiveDefinition(node) { + node.locations.forEach(location => { + const locationName = location.value; + // If a location already exists in the Set, then we've seen it once. + // This means we can remove it from the final diff, since both directives + // have this location in common. + if (locationsDiff.has(locationName)) { + locationsDiff.delete(locationName); + } else { + locationsDiff.add(locationName); + } + }); + + if (!node.arguments) return; + + // Arguments must have the same name and type. As matches are found, they + // are deleted from the diff. Anything left in the diff after looping + // represents a discrepancy between the two sets of arguments. + node.arguments.forEach(argument => { + const argumentName = argument.name.value; + const printedType = print(argument.type); + if (argumentsDiff[argumentName]) { + if (printedType === argumentsDiff[argumentName][0]) { + // If the existing entry is equal to printedType, it means there's no + // diff, so we can remove the entry from the diff object + delete argumentsDiff[argumentName]; + } else { + argumentsDiff[argumentName].push(printedType); + } + } else { + argumentsDiff[argumentName] = [printedType]; + } + }); + }, + }); + + const typeNameDiff = + firstNode.name.value === secondNode.name.value + ? [] + : [firstNode.name.value, secondNode.name.value]; + + const kindDiff = + firstNode.kind === secondNode.kind ? [] : [firstNode.kind, secondNode.kind]; + + return { + name: typeNameDiff, + kind: kindDiff, + fields: fieldsDiff, + unionTypes: unionTypesDiff, + locations: Array.from(locationsDiff), + args: argumentsDiff, + }; +} + +/** + * A common implementation of diffTypeNodes to ensure two type nodes are equivalent + * + * @param firstNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode + * @param secondNode TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode + */ +export function typeNodesAreEquivalent( + firstNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, + secondNode: TypeDefinitionNode | TypeExtensionNode | DirectiveDefinitionNode, +) { + const { name, kind, fields, unionTypes, locations, args } = diffTypeNodes( + firstNode, + secondNode, + ); + + return ( + name.length === 0 && + kind.length === 0 && + Object.keys(fields).length === 0 && + Object.keys(unionTypes).length === 0 && + locations.length === 0 && + Object.keys(args).length === 0 + ); +} + +/** + * A map of `Kind`s from their definition to their respective extensions + */ +export const defKindToExtKind: { [kind: string]: string } = { + [Kind.SCALAR_TYPE_DEFINITION]: Kind.SCALAR_TYPE_EXTENSION, + [Kind.OBJECT_TYPE_DEFINITION]: Kind.OBJECT_TYPE_EXTENSION, + [Kind.INTERFACE_TYPE_DEFINITION]: Kind.INTERFACE_TYPE_EXTENSION, + [Kind.UNION_TYPE_DEFINITION]: Kind.UNION_TYPE_EXTENSION, + [Kind.ENUM_TYPE_DEFINITION]: Kind.ENUM_TYPE_EXTENSION, + [Kind.INPUT_OBJECT_TYPE_DEFINITION]: Kind.INPUT_OBJECT_TYPE_EXTENSION, +}; + +// Transform an object's values via a callback function +export function mapValues( + object: Record, + callback: (value: T) => U, +): Record { + const result: Record = Object.create(null); + + for (const [key, value] of Object.entries(object)) { + result[key] = callback(value); + } + + return result; +} + +export function isNotNullOrUndefined( + value: T | null | undefined, +): value is T { + return value !== null && typeof value !== 'undefined'; +} + +export const executableDirectiveLocations = [ + 'QUERY', + 'MUTATION', + 'SUBSCRIPTION', + 'FIELD', + 'FRAGMENT_DEFINITION', + 'FRAGMENT_SPREAD', + 'INLINE_FRAGMENT', + 'VARIABLE_DEFINITION', +]; + +export function isFederationDirective(directive: GraphQLDirective): boolean { + return federationDirectives.some(({ name }) => name === directive.name); +} + +export const reservedRootFields = ['_service', '_entities']; + +// Map of OperationTypeNode to its respective default root operation type name +export const defaultRootOperationNameLookup: { + [node in OperationTypeNode]: DefaultRootOperationTypeName; +} = { + query: 'Query', + mutation: 'Mutation', + subscription: 'Subscription', +}; + +// This function is overloaded for 3 different input types. Each input type +// maps to a particular return type, hence the overload. +export function getFederationMetadata(obj: GraphQLNamedType): FederationType | undefined; +export function getFederationMetadata(obj: GraphQLField): FederationField | undefined; +export function getFederationMetadata(obj: GraphQLDirective): FederationDirective | undefined; +export function getFederationMetadata(obj: any) { + if (typeof obj === "undefined") return undefined; + else if (isNamedType(obj)) return obj.extensions?.federation as FederationType | undefined; + else if (isDirective(obj)) return obj.extensions?.federation as FederationDirective | undefined; + else return obj.extensions?.federation as FederationField | undefined; +} diff --git a/federation-js/src/composition/validate/__tests__/tsconfig.json b/federation-js/src/composition/validate/__tests__/tsconfig.json new file mode 100644 index 000000000..7d619d563 --- /dev/null +++ b/federation-js/src/composition/validate/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [{ "path": "../../../../" }] +} diff --git a/federation-js/src/composition/validate/index.ts b/federation-js/src/composition/validate/index.ts new file mode 100644 index 000000000..fc9385c35 --- /dev/null +++ b/federation-js/src/composition/validate/index.ts @@ -0,0 +1,59 @@ +import { GraphQLSchema, GraphQLError, validateSchema } from 'graphql'; +import { ServiceDefinition } from '../types'; + +// import validators +import * as preNormalizationRules from './preNormalization'; +import * as preCompositionRules from './preComposition'; +import * as postCompositionRules from './postComposition'; + +const preNormalizationValidators = Object.values(preNormalizationRules); + +export function validateServicesBeforeNormalization( + services: ServiceDefinition[], +) { + const errors: GraphQLError[] = []; + + for (const serviceDefinition of services) { + for (const validator of preNormalizationValidators) { + errors.push(...validator(serviceDefinition)); + } + } + + return errors; +} + +const preCompositionValidators = Object.values(preCompositionRules); + +export const validateServicesBeforeComposition = ( + services: ServiceDefinition[], +) => { + const warningsOrErrors: GraphQLError[] = []; + + for (const serviceDefinition of services) { + for (const validator of preCompositionValidators) { + warningsOrErrors.push(...validator(serviceDefinition)); + } + } + + return warningsOrErrors; +}; + +const postCompositionValidators = Object.values(postCompositionRules); + +export const validateComposedSchema = ({ + schema, + serviceList, +}: { + schema: GraphQLSchema; + serviceList: ServiceDefinition[]; +}): GraphQLError[] => { + const warningsOrErrors: GraphQLError[] = []; + + // https://github.com/graphql/graphql-js/blob/4b55f10f16cc77302613e8ad67440259c68633df/src/type/validate.js#L56 + warningsOrErrors.push(...validateSchema(schema)); + for (const validator of postCompositionValidators) { + warningsOrErrors.push(...validator({ schema, serviceList })); + } + + return warningsOrErrors; +}; diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts new file mode 100644 index 000000000..695e24efb --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts @@ -0,0 +1,122 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { executableDirectivesIdentical } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('executableDirectivesIdentical', () => { + it('throws no errors when custom, executable directives are defined identically every service', () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + directive @instrument(tag: String!) on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @stream on FIELD + directive @instrument(tag: String!) on FIELD + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesIdentical({ schema, serviceList }); + expect(errors).toHaveLength(0); + }); + + it('throws no errors when directives (excluding their TypeSystemDirectiveLocations) are identical for every service', () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + directive @instrument(tag: String!) on FIELD | FIELD_DEFINITION + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @stream on FIELD + directive @instrument(tag: String!) on FIELD + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesIdentical({ schema, serviceList }); + expect(errors).toHaveLength(0); + }); + + it("throws errors when custom, executable directives aren't defined with the same locations in every service", () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @stream on FIELD | QUERY + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + directive @stream on INLINE_FRAGMENT + `, + name: 'serviceC', + }; + + const serviceList = [serviceA, serviceB, serviceC]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesIdentical({ schema, serviceList }); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", + "message": "[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: + serviceA: directive @stream on FIELD + serviceB: directive @stream on FIELD | QUERY + serviceC: directive @stream on INLINE_FRAGMENT", + }, + ] + `); + }); + + it("throws errors when custom, executable directives aren't defined with the same arguments in every service", () => { + const serviceA = { + typeDefs: gql` + directive @instrument(tag: String!) on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @instrument(tag: Boolean) on FIELD + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesIdentical({ schema, serviceList }); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", + "message": "[@instrument] -> custom directives must be defined identically across all services. See below for a list of current implementations: + serviceA: directive @instrument(tag: String!) on FIELD + serviceB: directive @instrument(tag: Boolean) on FIELD", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts new file mode 100644 index 000000000..3e090dcab --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts @@ -0,0 +1,91 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { executableDirectivesInAllServices } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('executableDirectivesInAllServices', () => { + it('throws no errors when custom, executable directives are defined in every service', () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @stream on FIELD + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesInAllServices({ schema, serviceList }); + expect(errors).toHaveLength(0); + }); + + it("throws no errors when type system directives aren't defined in every service", () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + directive @stream on FIELD + # This directive is ignored by composition and therefore post-composition validators + directive @ignored on FIELD_DEFINITION + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesInAllServices({ schema, serviceList }); + expect(errors).toHaveLength(0); + }); + + it("throws errors when custom, executable directives aren't defined in every service", () => { + const serviceA = { + typeDefs: gql` + directive @stream on FIELD + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Query { + thing: String + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + extend type Query { + otherThing: String + } + `, + name: 'serviceC', + }; + + const serviceList = [serviceA, serviceB, serviceC]; + const { schema } = composeServices(serviceList); + const errors = executableDirectivesInAllServices({ schema, serviceList }); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES", + "message": "[@stream] -> Custom directives must be implemented in every service. The following services do not implement the @stream directive: serviceB, serviceC.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts new file mode 100644 index 000000000..5f9f086bb --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts @@ -0,0 +1,92 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { externalMissingOnBase as validateExternalMissingOnBase } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('externalMissingOnBase', () => { + it('warns when an @external field does not have a matching field on the base type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + id: String! @external + price: Int! @requires(fields: "sku id") + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + id: String! + test: Int @external + } + `, + name: 'serviceC', + }; + + const serviceList = [serviceA, serviceB, serviceC]; + const { schema } = composeServices([serviceA, serviceB, serviceC]); + const warnings = validateExternalMissingOnBase({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_MISSING_ON_BASE", + "message": "[serviceB] Product.id -> marked @external but id was defined in serviceC, not in the service that owns Product (serviceA)", + }, + Object { + "code": "EXTERNAL_MISSING_ON_BASE", + "message": "[serviceC] Product.test -> marked @external but test is not defined on the base service of Product (serviceA)", + }, + ] + `); + }); + + it("warns when an @external field isn't defined anywhere else", () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + specialId: String! @external + id: String! @requires(fields: "specialId") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalMissingOnBase({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_MISSING_ON_BASE", + "message": "[serviceB] Product.specialId -> marked @external but specialId is not defined on the base service of Product (serviceA)", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts new file mode 100644 index 000000000..ce59124e2 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts @@ -0,0 +1,76 @@ +import gql from 'graphql-tag'; +import { externalTypeMismatch as validateExternalTypeMismatch } from '../'; +import { composeServices } from '../../../compose'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('validateExternalDirectivesOnSchema', () => { + it('warns when the type of an @external field doesnt match the base', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalTypeMismatch({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_TYPE_MISMATCH", + "message": "[serviceB] Product.sku -> Type \`String\` does not match the type of the original field in serviceA (\`String!\`)", + }, + ] + `); + }); + + it("warns when an @external field's type does not exist in the composed schema", () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: NonExistentType! @external + id: String! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalTypeMismatch({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_TYPE_MISMATCH", + "message": "[serviceB] Product.sku -> the type of the @external field does not exist in the resulting composed schema", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts new file mode 100644 index 000000000..e3156c017 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -0,0 +1,380 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { externalUnused as validateExternalUnused } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('externalUnused', () => { + it('warns when there is an unused @external field', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "id") { + sku: String! + upc: String! + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + id: ID! @external + price: Int! @requires(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_UNUSED", + "message": "[serviceB] Product.sku -> is marked as @external but is not used by a @requires, @key, or @provides directive.", + }, + ] + `); + }); + + it('does not warn when @external is selected by a @key', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Float! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not warn when @external is selected by a @requires', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not warn when @external is selected by a @provides', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + id: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! @provides(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not warn when @external is selected by a @provides used from another type', () => { + const serviceA = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + username: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Review { + author: User @provides(fields: "username") + } + + extend type User @key(fields: "id") { + username: String @external + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it.todo( + 'does not error when @provides selects an external field in a subselection', + ); + + it.todo('errors when there is an invalid selection in @requires'); + + it('does not warn when @external is selected by a @requires used from another type', () => { + const serviceA = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + username: String + } + + type AccountRoles { + canRead: Boolean + canWrite: Boolean + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Review { + author: User + } + + extend type User @key(fields: "id") { + roles: AccountRoles! + isAdmin: Boolean! @requires(fields: "roles { canWrite }") + } + + # Externals -- only referenced by the @requires on User.isAdmin + extend type AccountRoles { + canWrite: Boolean @external + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not warn when @external is selected by a @requires in a deep subselection', () => { + const serviceA = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + username: String + } + + type AccountRoles { + canRead: Group + canWrite: Group + } + + type Group { + id: ID! + name: String + members: [User] + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Review { + author: User + } + + extend type User @key(fields: "id") { + id: ID! @external + roles: AccountRoles! + username: String @external + isAdmin: Boolean! + @requires( + fields: """ + roles { + canWrite { + members { + username + } + } + canRead { + members { + username + } + } + } + """ + ) + } + + # Externals -- only referenced by the @requires on User.isAdmin + extend type AccountRoles { + canWrite: Group @external + canRead: Group @external + } + + extend type Group { + members: [User] @external + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not warn when @external is used on type with multiple @key directives', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "upc") @key(fields: "sku") { + upc: String + sku: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "upc") { + upc: String @external + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String @external + } + `, + name: 'serviceC', + }; + + const serviceList = [serviceA, serviceB, serviceC]; + const { schema } = composeServices(serviceList); + const warnings = validateExternalUnused({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('does not error when @external is used on a field of a concrete type that implements a shared field of an implemented interface', () => { + const serviceA = { + typeDefs: gql` + type Car implements Vehicle @key(fields: "id") { + id: ID! + speed: Int + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + extend type Car implements Vehicle @key(fields: "id") { + id: ID! @external + speed: Int @external + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceB', + }; + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = validateExternalUnused({ schema, serviceList }); + expect(errors).toHaveLength(0); + }); + + it('does error when @external is used on a field of a concrete type is not shared by its implemented interface', () => { + const serviceA = { + typeDefs: gql` + type Car implements Vehicle @key(fields: "id") { + id: ID! + speed: Int + wheelSize: Int + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + extend type Car implements Vehicle @key(fields: "id") { + id: ID! @external + speed: Int @external + wheelSize: Int @external + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceB', + }; + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = validateExternalUnused({ schema, serviceList }); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_UNUSED", + "message": "[serviceB] Car.wheelSize -> is marked as @external but is not used by a @requires, @key, or @provides directive.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts new file mode 100644 index 000000000..5b3229403 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -0,0 +1,113 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { keyFieldsMissingOnBase as validateKeyFieldsMissingOnBase } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('keyFieldsMissingOnBase', () => { + it('returns no warnings with proper @key usage', () => { + const serviceA = { + // FIXME: add second key "upc" when duplicate directives are supported + // i.e. @key(fields: "sku") @key(fields: "upc") + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); + expect(warnings).toHaveLength(0); + }); + + it('warns if @key references a field added by another service', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku uid") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + uid: String! + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_MISSING_ON_BASE", + "message": "[serviceA] Product -> A @key selects uid, but Product.uid was either created or overwritten by serviceB, not serviceA", + }, + ] + `); + }); + + // FIXME: shouldn't composition _allow_ this with a warning? + // right now, it errors during composition + xit('warns if @key references a field that was overwritten', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: ID! # overwritten from base service + weight: Float! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsMissingOnBase({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts new file mode 100644 index 000000000..2d77ff89f --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -0,0 +1,132 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { keyFieldsSelectInvalidType as validateKeyFieldsSelectInvalidType } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('keyFieldsSelectInvalidType', () => { + it('returns no warnings with proper @key usage', () => { + const serviceA = { + // FIXME: add second key "upc" when duplicate directives are supported + // i.e. @key(fields: "sku") @key(fields: "upc") + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toHaveLength(0); + }); + + it('warns if @key references fields of an interface type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "featuredItem") { + featuredItem: Node! + sku: String! + } + + interface Node { + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + price: Int! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_SELECT_INVALID_TYPE", + "message": "[serviceA] Product -> A @key selects Product.featuredItem, which is an interface type. Keys cannot select interfaces.", + }, + ] + `); + }); + + it('warns if @key references fields of a union type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "price") { + sku: String! + price: Numeric! + } + + union Numeric = Float | Int + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + sku: String! @external + name: String! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateKeyFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_SELECT_INVALID_TYPE", + "message": "[serviceA] Product -> A @key selects Product.price, which is a union type. Keys cannot select union types.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts new file mode 100644 index 000000000..5c2854f64 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -0,0 +1,117 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { keysMatchBaseService as validateKeysMatchBaseService } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('keysMatchBaseService', () => { + it('returns no errors with proper @key usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const validationErrors = validateKeysMatchBaseService({ + schema, + serviceList, + }); + expect(validationErrors).toHaveLength(0); + }); + + it('requires a @key to be specified on the originating type', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const validationErrors = validateKeysMatchBaseService({ + schema, + serviceList, + }); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0]).toMatchInlineSnapshot(` + Object { + "code": "KEY_MISSING_ON_BASE", + "message": "[serviceA] Product -> appears to be an entity but no @key directives are specified on the originating type.", + } + `); + }); + + it('requires extending services to use a @key specified by the originating type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku upc") { + sku: String! + upc: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const validationErrors = validateKeysMatchBaseService({ + schema, + serviceList, + }); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0]).toMatchInlineSnapshot(` + Object { + "code": "KEY_NOT_SPECIFIED", + "message": "[serviceB] Product -> extends from serviceA but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are: + @key(fields: \\"sku upc\\")", + } + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts new file mode 100644 index 000000000..0eed1d298 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts @@ -0,0 +1,106 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { providesFieldsMissingExternal as validateProdivesFieldsMissingExternal } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('providesFieldsMissingExternal', () => { + it('does not warn with proper @provides usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + username: String + } + `, + name: 'serviceB', + }; + + const serviceC = { + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + product: Product @provides(fields: "id") + author: User @provides(fields: "username") + } + + extend type Product @key(fields: "sku") { + sku: String! @external + id: ID! @external + price: Int! + } + + extend type User @key(fields: "id") { + id: ID! @external + username: String @external + } + `, + name: 'serviceC', + }; + + const serviceList = [serviceA, serviceB, serviceC]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toEqual([]); + const warnings = validateProdivesFieldsMissingExternal({ + schema, + serviceList, + }); + expect(warnings).toEqual([]); + }); + + it('warns when there is a @provides with no matching @external field', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + product: Product @provides(fields: "id") + } + + extend type Product @key(fields: "sku") { + sku: String! @external + price: Int! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toEqual([]); + const warnings = validateProdivesFieldsMissingExternal({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_FIELDS_MISSING_EXTERNAL", + "message": "[serviceB] Review.product -> provides the field \`id\` and requires Product.id to be marked as @external.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts new file mode 100644 index 000000000..364b491aa --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts @@ -0,0 +1,214 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { providesFieldsSelectInvalidType as validateprovidesFieldsSelectInvalidType } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('providesFieldsSelectInvalidType', () => { + it('returns no warnings with proper @provides usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + upc: String! @external + price: Int! @provides(fields: "upc") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateprovidesFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toHaveLength(0); + }); + + it('warns if @provides references fields of a list type', () => { + const serviceA = { + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + author: User @provides(fields: "wishLists") + } + + extend type User @key(fields: "id") { + id: ID! @external + wishLists: [WishList] @external + } + + extend type WishList @key(fields: "id") { + id: ID! @external + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + wishLists: [WishList] + } + + type WishList @key(fields: "id") { + id: ID! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateprovidesFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "message": "[serviceA] Review.author -> A @provides selects User.wishLists, which is a list type. A field cannot @provide lists.", + }, + ] + `); + }); + + it('warns if @provides references fields of an interface type', () => { + const serviceA = { + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + author: User @provides(fields: "account") + } + + extend type User @key(fields: "id") { + id: ID! @external + account: Account @external + } + + extend interface Account { + username: String @external + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + account: Account + } + + interface Account { + username: String + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateprovidesFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "message": "[serviceA] Review.author -> A @provides selects User.account, which is an interface type. A field cannot @provide interfaces.", + }, + ] + `); + }); + + it('warns if @provides references fields of a union type', () => { + const serviceA = { + typeDefs: gql` + type Review @key(fields: "id") { + id: ID! + author: User @provides(fields: "account") + } + + extend type User @key(fields: "id") { + id: ID! @external + account: Account @external + } + + extend union Account = PasswordAccount | SMSAccount + + extend type PasswordAccount @key(fields: "email") { + email: String! @external + } + + extend type SMSAccount @key(fields: "phone") { + phone: String! @external + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + account: Account + } + + union Account = PasswordAccount | SMSAccount + + type PasswordAccount @key(fields: "email") { + email: String! + } + + type SMSAccount @key(fields: "phone") { + phone: String! + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema, errors } = composeServices(serviceList); + expect(errors).toHaveLength(0); + + const warnings = validateprovidesFieldsSelectInvalidType({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "message": "[serviceA] Review.author -> A @provides selects User.account, which is a union type. A field cannot @provide union types.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts new file mode 100644 index 000000000..1a87fc985 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts @@ -0,0 +1,259 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { providesNotOnEntity as validateProvidesNotOnEntity } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('providesNotOnEntity', () => { + it('does not warn when @provides used on an entity', () => { + const serviceA = { + typeDefs: gql` + type LineItem @key(fields: "sku") { + sku: String! + quantity: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + lineItem: LineItem @provides(fields: "quantity") + lineItemNonNull: LineItem! @provides(fields: "quantity") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(`Array []`); + }); + + it('does not warn when @provides used on a list of entity', () => { + const serviceA = { + typeDefs: gql` + type LineItem @key(fields: "sku") { + sku: String! + quantity: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Product { + lineItems: [LineItem] @provides(fields: "quantity") + lineItemsNonNull: [LineItem]! @provides(fields: "quantity") + nonNullLineItems: [LineItem!] @provides(fields: "quantity") + nonNullLineItemsNonNull: [LineItem!]! @provides(fields: "quantity") + deep: [[LineItem!]!]! @provides(fields: "quantity") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(`Array []`); + }); + + it('does not warn when @provides used on an entity of a child type', () => { + const serviceA = { + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + username: String + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + type Review { + author: User @provides(fields: "username") + } + + type User { + username: String @external + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toEqual([]); + }); + + it('warns when there is a @provides on a type that is not an entity', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + + type LineItem { + sku: String! + quantity: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + lineItem: LineItem @provides(fields: "quantity") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_NOT_ON_ENTITY", + "message": "[serviceB] Product.lineItem -> uses the @provides directive but \`Product.lineItem\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", + }, + ] + `); + }); + + it('warns when there is a @provides on a type that is not a list of entity', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + + type LineItem { + sku: String! + quantity: Int! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + lineItems: [LineItem] @provides(fields: "quantity") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_NOT_ON_ENTITY", + "message": "[serviceB] Product.lineItems -> uses the @provides directive but \`Product.lineItems\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", + }, + ] + `); + }); + + it('warns when there is a @provides on a non-object type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + + enum Category { + BOOK + MOVIE + SONG + ALBUM + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + category: Category @provides(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_NOT_ON_ENTITY", + "message": "[serviceB] Product.category -> uses the @provides directive but \`Product.category\` returns \`Category\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", + }, + ] + `); + }); + + it('warns when there is a @provides on a list of non-object type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + + enum Category { + BOOK + MOVIE + SONG + ALBUM + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + categories: [Category] @provides(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateProvidesNotOnEntity({ schema, serviceList }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "PROVIDES_NOT_ON_ENTITY", + "message": "[serviceB] Product.categories -> uses the @provides directive but \`Product.categories\` returns \`[Category]\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts new file mode 100644 index 000000000..c1e824f81 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts @@ -0,0 +1,76 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { requiresFieldsMissingExternal as validateRequiresFieldsMissingExternal } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('requiresFieldsMissingExternal', () => { + it('does not warn with proper @requires usage', () => { + const serviceA = { + typeDefs: gql` + type Product { + sku: String! + upc: String! + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + id: ID! @external + price: Int! @requires(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateRequiresFieldsMissingExternal({ + schema, + serviceList, + }); + expect(warnings).toEqual([]); + }); + + it('warns when there is a @requires with no matching @external field', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! + id: ID! + } + `, + name: 'serviceA', + }; + + const serviceB = { + typeDefs: gql` + extend type Product { + price: Int! @requires(fields: "id") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateRequiresFieldsMissingExternal({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", + "message": "[serviceB] Product.price -> requires the field \`id\` to be marked as @external.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts new file mode 100644 index 000000000..945a50580 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts @@ -0,0 +1,79 @@ +import gql from 'graphql-tag'; +import { composeServices } from '../../../compose'; +import { requiresFieldsMissingOnBase as validateRequiresFieldsMissingOnBase } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('requiresFieldsMissingOnBase', () => { + it('does not warn with proper @requires usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + id: ID! + weight: Float! @requires(fields: "sku") + } + `, + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const warnings = validateRequiresFieldsMissingOnBase({ + schema, + serviceList, + }); + expect(warnings).toEqual([]); + }); + + it('warns when requires selects a field not found on the base type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + id: ID! + } + `, + name: 'serviceB', + }; + const serviceC = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + id: ID! @external + weight: Float! @requires(fields: "id") + } + `, + name: 'serviceC', + }; + const serviceList = [serviceA, serviceB, serviceC]; + const { schema } = composeServices(serviceList); + const warnings = validateRequiresFieldsMissingOnBase({ + schema, + serviceList, + }); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "REQUIRES_FIELDS_MISSING_ON_BASE", + "message": "[serviceC] Product.weight -> requires the field \`id\` to be @external. @external fields must exist on the base type, not an extension.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/postComposition/__tests__/tsconfig.json b/federation-js/src/composition/validate/postComposition/__tests__/tsconfig.json new file mode 100644 index 000000000..9bcedd51a --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [{ "path": "../../../../../" }] +} diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts new file mode 100644 index 000000000..a0fa2cb9f --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts @@ -0,0 +1,60 @@ +import 'apollo-server-env'; +import { GraphQLError, isSpecifiedDirective, print } from 'graphql'; +import { + errorWithCode, + isFederationDirective, + logDirective, + typeNodesAreEquivalent, + getFederationMetadata, +} from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * A custom directive must be defined identically across all services. This means + * they must have the same name and same locations. Locations are the "on" part of + * a directive, for example: + * directive @stream on FIELD | QUERY + */ +export const executableDirectivesIdentical: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const customDirectives = schema + .getDirectives() + .filter(x => !isFederationDirective(x) && !isSpecifiedDirective(x)); + + customDirectives.forEach(directive => { + const directiveFederationMetadata = getFederationMetadata(directive); + + if (!directiveFederationMetadata) return; + + const definitions = Object.entries( + directiveFederationMetadata.directiveDefinitions, + ); + + // Side-by-side compare all definitions of a single directive, if there's a + // discrepancy in any of those diffs, we should provide an error. + const shouldError = definitions.some(([, definition], index) => { + // Skip the non-comparison step + if (index === 0) return; + const [, previousDefinition] = definitions[index - 1]; + return !typeNodesAreEquivalent(definition, previousDefinition); + }); + + if (shouldError) { + errors.push( + errorWithCode( + 'EXECUTABLE_DIRECTIVES_IDENTICAL', + logDirective(directive.name) + + `custom directives must be defined identically across all services. See below for a list of current implementations:\n${definitions + .map(([serviceName, definition]) => { + return `\t${serviceName}: ${print(definition)}`; + }) + .join('\n')}`, + ), + ); + } + }); + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts new file mode 100644 index 000000000..131d0bdef --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts @@ -0,0 +1,60 @@ +import 'apollo-server-env'; +import { GraphQLError, isSpecifiedDirective } from 'graphql'; +import { + errorWithCode, + isFederationDirective, + logDirective, + getFederationMetadata, +} from '../../utils'; +import { PostCompositionValidator } from '.'; +/** + * All custom directives with executable locations must be implemented in every + * service. This validator is not responsible for ensuring the directives are an + * ExecutableDirective, however composition ensures this by filtering out all + * TypeSystemDirectiveLocations. + */ +export const executableDirectivesInAllServices: PostCompositionValidator = ({ + schema, + serviceList, +}) => { + const errors: GraphQLError[] = []; + + const customExecutableDirectives = schema + .getDirectives() + .filter(x => !isFederationDirective(x) && !isSpecifiedDirective(x)); + + customExecutableDirectives.forEach(directive => { + const directiveFederationMetadata = getFederationMetadata(directive); + + if (!directiveFederationMetadata) return; + + const allServiceNames = serviceList.map(({ name }) => name); + const serviceNamesWithDirective = Object.keys( + directiveFederationMetadata.directiveDefinitions, + ); + + const serviceNamesWithoutDirective = allServiceNames.reduce( + (without, serviceName) => { + if (!serviceNamesWithDirective.includes(serviceName)) { + without.push(serviceName); + } + return without; + }, + [] as string[], + ); + + if (serviceNamesWithoutDirective.length > 0) { + errors.push( + errorWithCode( + 'EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES', + logDirective(directive.name) + + `Custom directives must be implemented in every service. The following services do not implement the @${ + directive.name + } directive: ${serviceNamesWithoutDirective.join(', ')}.`, + ), + ); + } + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts new file mode 100644 index 000000000..1dcaa7bb8 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts @@ -0,0 +1,62 @@ +import 'apollo-server-env'; +import { isObjectType, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * All fields marked with @external must exist on the base type + */ +export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + const typeFederationMetadata = getFederationMetadata(namedType); + + // If externals is populated, we need to look at each one and confirm + // that field exists on base service + if (typeFederationMetadata?.externals) { + // loop over every service that has extensions with @external + for (const [serviceName, externalFieldsForService] of Object.entries( + typeFederationMetadata.externals, + )) { + // for a single service, loop over the external fields. + for (const { field: externalField } of externalFieldsForService) { + const externalFieldName = externalField.name.value; + const allFields = namedType.getFields(); + const matchingBaseField = allFields[externalFieldName]; + + // @external field referenced a field that isn't defined anywhere + if (!matchingBaseField) { + errors.push( + errorWithCode( + 'EXTERNAL_MISSING_ON_BASE', + logServiceAndType(serviceName, typeName, externalFieldName) + + `marked @external but ${externalFieldName} is not defined on the base service of ${typeName} (${typeFederationMetadata.serviceName})`, + ), + ); + continue; + } + + // if the field has a serviceName, then it wasn't defined by the + // service that owns the type + const fieldFederationMetadata = getFederationMetadata(matchingBaseField); + + if (fieldFederationMetadata?.serviceName) { + errors.push( + errorWithCode( + 'EXTERNAL_MISSING_ON_BASE', + logServiceAndType(serviceName, typeName, externalFieldName) + + `marked @external but ${externalFieldName} was defined in ${fieldFederationMetadata.serviceName}, not in the service that owns ${typeName} (${typeFederationMetadata.serviceName})`, + ), + ); + } + } + } + } + } + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts new file mode 100644 index 000000000..b076bdc17 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts @@ -0,0 +1,65 @@ +import { isObjectType, typeFromAST, isEqualType, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * All fields marked with @external must match the type definition of the base service. + * Additional warning if the type of the @external field doesn't exist at all on the schema + */ +export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + // If externals is populated, we need to look at each one and confirm + // there is a matching @requires + const typeFederationMetadata = getFederationMetadata(namedType); + if (typeFederationMetadata?.externals) { + // loop over every service that has extensions with @external + for (const [serviceName, externalFieldsForService] of Object.entries( + typeFederationMetadata.externals, + )) { + // for a single service, loop over the external fields. + for (const { field: externalField } of externalFieldsForService) { + const externalFieldName = externalField.name.value; + const allFields = namedType.getFields(); + const matchingBaseField = allFields[externalFieldName]; + + // FIXME: TypeScript doesn’t currently support passing in a type union + // to an overloaded function like `typeFromAST` + // See https://github.com/Microsoft/TypeScript/issues/14107 + const externalFieldType = typeFromAST( + schema, + externalField.type as any, + ); + + if (!externalFieldType) { + errors.push( + errorWithCode( + 'EXTERNAL_TYPE_MISMATCH', + logServiceAndType(serviceName, typeName, externalFieldName) + + `the type of the @external field does not exist in the resulting composed schema`, + ), + ); + } else if ( + matchingBaseField && + !isEqualType(matchingBaseField.type, externalFieldType) + ) { + errors.push( + errorWithCode( + 'EXTERNAL_TYPE_MISMATCH', + logServiceAndType(serviceName, typeName, externalFieldName) + + `Type \`${externalFieldType.name}\` does not match the type of the original field in ${typeFederationMetadata.serviceName} (\`${matchingBaseField.type}\`)`, + ), + ); + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/externalUnused.ts b/federation-js/src/composition/validate/postComposition/externalUnused.ts new file mode 100644 index 000000000..2c671ef21 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/externalUnused.ts @@ -0,0 +1,237 @@ +import { isObjectType, GraphQLError, Kind } from 'graphql'; +import { + findDirectivesOnTypeOrField, + logServiceAndType, + hasMatchingFieldInDirectives, + errorWithCode, + findFieldsThatReturnType, + parseSelections, + isStringValueNode, + selectionIncludesField, + getFederationMetadata, +} from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * for every @external field, there should be a @requires, @key, or @provides + * directive that uses it + */ +export const externalUnused: PostCompositionValidator = ({ schema }) => { + const errors: GraphQLError[] = []; + const types = schema.getTypeMap(); + for (const [parentTypeName, parentType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(parentType)) continue; + // If externals is populated, we need to look at each one and confirm + // it is used + const typeFederationMetadata = getFederationMetadata(parentType); + + // Escape a validation case that's falling through incorrectly. This case + // is handled by `keysMatchBaseService`. + if (typeFederationMetadata) { + const {serviceName, keys} = typeFederationMetadata; + if (serviceName && keys && !keys[serviceName]) continue; + } + + if (typeFederationMetadata?.externals) { + // loop over every service that has extensions with @external + for (const [serviceName, externalFieldsForService] of Object.entries( + typeFederationMetadata.externals, + )) { + // for a single service, loop over the external fields. + for (const { field: externalField } of externalFieldsForService) { + const externalFieldName = externalField.name.value; + + // check the selected fields of every @key provided by `serviceName` + const hasMatchingKeyOnType = Boolean( + hasMatchingFieldInDirectives({ + directives: findDirectivesOnTypeOrField( + parentType.astNode, + 'key', + ), + fieldNameToMatch: externalFieldName, + namedType: parentType, + }), + ); + if (hasMatchingKeyOnType) continue; + + /* + @provides is most commonly used from another type than where + the @external directive is applied. We need to find all + fields on any type in the schema that return this type + and see if they have a provides directive that uses this + external field + + extend type Review { + author: User @provides(fields: "username") + } + + extend type User @key(fields: "id") { + id: ID! @external + username: String @external + reviews: [Review] + } + */ + const hasMatchingProvidesOnAnotherType = findFieldsThatReturnType({ + schema, + typeToFind: parentType, + }).some(field => + findDirectivesOnTypeOrField(field.astNode, 'provides').some( + directive => { + if (!directive.arguments) return false; + const selections = + isStringValueNode(directive.arguments[0].value) && + parseSelections(directive.arguments[0].value.value); + // find the selections which are fields with names matching + // our external field name + return ( + selections && + selections.some( + selection => + selection.kind === Kind.FIELD && + selection.name.value === externalFieldName, + ) + ); + }, + ), + ); + + if (hasMatchingProvidesOnAnotherType) continue; + + /** + * @external fields can be selected by subfields of a selection on another type + * + * For example, with these defs, `canWrite` is marked as external and is + * referenced by a selection set inside the @requires of User.isAdmin + * + * extend type User @key(fields: "id") { + * roles: AccountRoles! + * isAdmin: Boolean! @requires(fields: "roles { canWrite permission { status } }") + * } + * extend type AccountRoles { + * canWrite: Boolean @external + * permission: Permission @external + * } + * + * extend type Permission { + * status: String @external + * } + * + * So, we need to search for fields with requires, then parse the selection sets, + * and try to recursively find the external field's PARENT type, then the external field's name + */ + const hasMatchingRequiresOnAnotherType = Object.values( + schema.getTypeMap(), + ).some(namedType => { + if (!isObjectType(namedType)) return false; + // for every object type, loop over its fields and find fields + // with requires directives + return Object.values(namedType.getFields()).some(field => + findDirectivesOnTypeOrField(field.astNode, 'requires').some( + directive => { + if (!directive.arguments) return false; + const selections = + isStringValueNode(directive.arguments[0].value) && + parseSelections(directive.arguments[0].value.value); + + if (!selections) return false; + return selectionIncludesField({ + selections, + selectionSetType: namedType, + typeToFind: parentType, + fieldToFind: externalFieldName, + }); + }, + ), + ); + }); + + if (hasMatchingRequiresOnAnotherType) continue; + + const hasMatchingRequiresOnType = Object.values( + parentType.getFields(), + ).some(maybeRequiresField => { + const fieldOwner = getFederationMetadata(maybeRequiresField)?.serviceName; + if (fieldOwner !== serviceName) return false; + + const requiresDirectives = findDirectivesOnTypeOrField( + maybeRequiresField.astNode, + 'requires', + ); + + return hasMatchingFieldInDirectives({ + directives: requiresDirectives, + fieldNameToMatch: externalFieldName, + namedType: parentType, + }); + }); + + if (hasMatchingRequiresOnType) continue; + + /** + * @external fields can be required when an interface is returned by + * a field and its concrete implementations need to be defined in a + * service which use non-key fields from other services. Take for example: + * + * // Service A + * type Car implements Vehicle @key(fields: "id") { + * id: ID! + * speed: Int + * } + * + * interface Vehicle { + * id: ID! + * speed: Int + * } + * + * // Service B + * type Query { + * vehicles: [Vehicle] + * } + * + * extend type Car implements Vehicle @key(fields: "id") { + * id: ID! @external + * speed: Int @external + * } + * + * interface Vehicle { + * id: ID! + * speed: Int + * } + * + * Service B defines Car.speed as an external field which is okay + * because it is required for Query.vehicles to exist in the schema + */ + const fieldsOnInterfacesImplementedByParentType: Set = new Set(); + + // Loop over the parent's interfaces + for (const _interface of parentType.getInterfaces()) { + // Collect the field names from each interface in a set + for (const fieldName in _interface.getFields()) { + fieldsOnInterfacesImplementedByParentType.add(fieldName); + } + } + + // If the set contains our field's name, no error is generated + if (fieldsOnInterfacesImplementedByParentType.has(externalFieldName)) { + continue; + } + + errors.push( + errorWithCode( + 'EXTERNAL_UNUSED', + logServiceAndType( + serviceName, + parentTypeName, + externalFieldName, + ) + + `is marked as @external but is not used by a @requires, @key, or @provides directive.`, + ), + ); + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/index.ts b/federation-js/src/composition/validate/postComposition/index.ts new file mode 100644 index 000000000..6e7a35189 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/index.ts @@ -0,0 +1,28 @@ +import { GraphQLSchema, GraphQLError } from 'graphql'; +import { ServiceDefinition } from '../../types'; + +export { externalUnused } from './externalUnused'; +export { externalMissingOnBase } from './externalMissingOnBase'; +export { externalTypeMismatch } from './externalTypeMismatch'; +export { requiresFieldsMissingExternal } from './requiresFieldsMissingExternal'; +export { requiresFieldsMissingOnBase } from './requiresFieldsMissingOnBase'; +export { keyFieldsMissingOnBase } from './keyFieldsMissingOnBase'; +export { keyFieldsSelectInvalidType } from './keyFieldsSelectInvalidType'; +export { providesFieldsMissingExternal } from './providesFieldsMissingExternal'; +export { + providesFieldsSelectInvalidType, +} from './providesFieldsSelectInvalidType'; +export { providesNotOnEntity } from './providesNotOnEntity'; +export { + executableDirectivesInAllServices, +} from './executableDirectivesInAllServices'; +export { executableDirectivesIdentical } from './executableDirectivesIdentical'; +export { keysMatchBaseService } from './keysMatchBaseService'; + +export type PostCompositionValidator = ({ + schema, + serviceList, +}: { + schema: GraphQLSchema; + serviceList: ServiceDefinition[]; +}) => GraphQLError[]; diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts new file mode 100644 index 000000000..a2d7384a6 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -0,0 +1,52 @@ +import { isObjectType, FieldNode, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * - The fields argument can not select fields that were overwritten by another service + */ +export const keyFieldsMissingOnBase: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + if (!isObjectType(namedType)) continue; + + const typeFederationMetadata = getFederationMetadata(namedType); + if (typeFederationMetadata?.keys) { + const allFieldsInType = namedType.getFields(); + for (const [serviceName, selectionSets] of Object.entries( + typeFederationMetadata.keys, + )) { + for (const selectionSet of selectionSets) { + for (const field of selectionSet as FieldNode[]) { + const name = field.name.value; + + // find corresponding field for each selected field + const matchingField = allFieldsInType[name]; + + // NOTE: We don't need to warn if there is no matching field. + // keyFieldsSelectInvalidType already does that :) + if (matchingField) { + const fieldFederationMetadata = getFederationMetadata(matchingField); + // warn if not from base type OR IF IT WAS OVERWITTEN + if (fieldFederationMetadata?.serviceName) { + errors.push( + errorWithCode( + 'KEY_FIELDS_MISSING_ON_BASE', + logServiceAndType(serviceName, typeName) + + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, + ), + ); + } + } + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts new file mode 100644 index 000000000..63d089ca9 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -0,0 +1,85 @@ +import { + isObjectType, + FieldNode, + isInterfaceType, + isNonNullType, + getNullableType, + isUnionType, + GraphQLError, +} from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * - The fields argument can not have root fields that result in a list + * - The fields argument can not have root fields that result in an interface + * - The fields argument can not have root fields that result in a union type + */ +export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + if (!isObjectType(namedType)) continue; + + const typeFederationMetadata = getFederationMetadata(namedType); + if (typeFederationMetadata?.keys) { + const allFieldsInType = namedType.getFields(); + for (const [serviceName, selectionSets] of Object.entries( + typeFederationMetadata.keys, + )) { + for (const selectionSet of selectionSets) { + for (const field of selectionSet as FieldNode[]) { + const name = field.name.value; + + // find corresponding field for each selected field + const matchingField = allFieldsInType[name]; + if (!matchingField) { + errors.push( + errorWithCode( + 'KEY_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName) + + `A @key selects ${name}, but ${typeName}.${name} could not be found`, + ), + ); + } + + if (matchingField) { + if ( + isInterfaceType(matchingField.type) || + (isNonNullType(matchingField.type) && + isInterfaceType(getNullableType(matchingField.type))) + ) { + errors.push( + errorWithCode( + 'KEY_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName) + + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, + ), + ); + } + + if ( + isUnionType(matchingField.type) || + (isNonNullType(matchingField.type) && + isUnionType(getNullableType(matchingField.type))) + ) { + errors.push( + errorWithCode( + 'KEY_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName) + + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, + ), + ); + } + } + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts new file mode 100644 index 000000000..62e4cc575 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -0,0 +1,86 @@ +import { isObjectType, GraphQLError, SelectionNode } from 'graphql'; +import { + logServiceAndType, + errorWithCode, + getFederationMetadata, +} from '../../utils'; +import { PostCompositionValidator } from '.'; +import { printWithReducedWhitespace } from '../../../service'; + +/** + * 1. KEY_MISSING_ON_BASE - Originating types must specify at least 1 @key directive + * 2. MULTIPLE_KEYS_ON_EXTENSION - Extending services may not use more than 1 @key directive + * 3. KEY_NOT_SPECIFIED - Extending services must use a valid @key specified by the originating type + */ +export const keysMatchBaseService: PostCompositionValidator = function ({ + schema, +}) { + const errors: GraphQLError[] = []; + const types = schema.getTypeMap(); + for (const [parentTypeName, parentType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(parentType)) continue; + + const typeFederationMetadata = getFederationMetadata(parentType); + + if (typeFederationMetadata) { + const { serviceName, keys } = typeFederationMetadata; + + if (serviceName && keys) { + if (!keys[serviceName]) { + errors.push( + errorWithCode( + 'KEY_MISSING_ON_BASE', + logServiceAndType(serviceName, parentTypeName) + + `appears to be an entity but no @key directives are specified on the originating type.`, + ), + ); + continue; + } + + const availableKeys = keys[serviceName].map(printFieldSet); + Object.entries(keys) + // No need to validate that the owning service matches its specified keys + .filter(([service]) => service !== serviceName) + .forEach(([extendingService, keyFields]) => { + // Extensions can't specify more than one key + if (keyFields.length > 1) { + errors.push( + errorWithCode( + 'MULTIPLE_KEYS_ON_EXTENSION', + logServiceAndType(extendingService, parentTypeName) + + `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, + ), + ); + return; + } + + // This isn't representative of an invalid graph, but it is an existing + // limitation of the query planner that we want to validate against for now. + // In the future, `@key`s just need to be "reachable" through a number of + // services which can link one key to another via "joins". + const extensionKey = printFieldSet(keyFields[0]); + if (!availableKeys.includes(extensionKey)) { + errors.push( + errorWithCode( + 'KEY_NOT_SPECIFIED', + logServiceAndType(extendingService, parentTypeName) + + `extends from ${serviceName} but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n` + + `\t${availableKeys + .map((fieldSet) => `@key(fields: "${fieldSet}")`) + .join('\n\t')}`, + ), + ); + return; + } + }); + } + } + } + + return errors; +}; + +function printFieldSet(selections: readonly SelectionNode[]): string { + return selections.map(printWithReducedWhitespace).join(' '); +} diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts new file mode 100644 index 000000000..6a4cb2eef --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts @@ -0,0 +1,60 @@ +import { isObjectType, FieldNode, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * for every field in a @provides, there should be a matching @external + */ +export const providesFieldsMissingExternal: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + // for each field, if there's a requires on it, check that there's a matching + // @external field, and that the types referenced are from the base type + for (const [fieldName, field] of Object.entries(namedType.getFields())) { + const fieldFederationMetadata = getFederationMetadata(field); + const serviceName = fieldFederationMetadata?.serviceName; + + // serviceName should always exist on fields that have @provides federation data, since + // the only case where serviceName wouldn't exist is on a base type, and in that case, + // the `provides` metadata should never get added to begin with. This should be caught in + // composition work. This kind of error should be validated _before_ composition. + if (!serviceName) continue; + + const fieldType = field.type; + if (!isObjectType(fieldType)) continue; + + const fieldTypeFederationMetadata = getFederationMetadata(fieldType); + + const externalFieldsOnTypeForService = fieldTypeFederationMetadata?.externals?.[serviceName]; + + if (fieldFederationMetadata?.provides) { + const selections = fieldFederationMetadata.provides as FieldNode[]; + for (const selection of selections) { + const foundMatchingExternal = externalFieldsOnTypeForService + ? externalFieldsOnTypeForService.some( + ext => ext.field.name.value === selection.name.value, + ) + : undefined; + if (!foundMatchingExternal) { + errors.push( + errorWithCode( + 'PROVIDES_FIELDS_MISSING_EXTERNAL', + logServiceAndType(serviceName, typeName, fieldName) + + `provides the field \`${selection.name.value}\` and requires ${fieldType}.${selection.name.value} to be marked as @external.`, + ), + ); + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts new file mode 100644 index 000000000..8b308475e --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -0,0 +1,106 @@ +import { + GraphQLError, + isObjectType, + FieldNode, + isListType, + isInterfaceType, + isNonNullType, + getNullableType, + isUnionType, +} from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * - The fields argument can not have root fields that result in a list + * - The fields argument can not have root fields that result in an interface + * - The fields argument can not have root fields that result in a union type + */ +export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + if (!isObjectType(namedType)) continue; + + // for each field, if there's a provides on it, check the type of the field + // it references + for (const [fieldName, field] of Object.entries(namedType.getFields())) { + const fieldFederationMetadata = getFederationMetadata(field); + const serviceName = fieldFederationMetadata?.serviceName; + + // serviceName should always exist on fields that have @provides federation data, since + // the only case where serviceName wouldn't exist is on a base type, and in that case, + // the `provides` metadata should never get added to begin with. This should be caught in + // composition work. This kind of error should be validated _before_ composition. + if (!serviceName) continue; + + const fieldType = field.type; + if (!isObjectType(fieldType)) continue; + const allFields = fieldType.getFields(); + + if (fieldFederationMetadata?.provides) { + const selections = fieldFederationMetadata.provides as FieldNode[]; + for (const selection of selections) { + const name = selection.name.value; + const matchingField = allFields[name]; + if (!matchingField) { + errors.push( + errorWithCode( + 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName, fieldName) + + `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, + ), + ); + continue; + } + + if ( + isListType(matchingField.type) || + (isNonNullType(matchingField.type) && + isListType(getNullableType(matchingField.type))) + ) { + errors.push( + errorWithCode( + 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName, fieldName) + + `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, + ), + ); + } + if ( + isInterfaceType(matchingField.type) || + (isNonNullType(matchingField.type) && + isInterfaceType(getNullableType(matchingField.type))) + ) { + errors.push( + errorWithCode( + 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName, fieldName) + + `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, + ), + ); + } + + if ( + isUnionType(matchingField.type) || + (isNonNullType(matchingField.type) && + isUnionType(getNullableType(matchingField.type))) + ) { + errors.push( + errorWithCode( + 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', + logServiceAndType(serviceName, typeName, fieldName) + + `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, + ), + ); + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts new file mode 100644 index 000000000..6fd8f362a --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts @@ -0,0 +1,77 @@ +import { + isObjectType, + GraphQLError, + isListType, + isNonNullType, +} from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * Provides directive can only be added to return types that are entities + */ +export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + // for each field, if there's a provides on it, check that the containing + // type has a `key` field under the federation metadata. + for (const [fieldName, field] of Object.entries(namedType.getFields())) { + const fieldFederationMetadata = getFederationMetadata(field) + const serviceName = fieldFederationMetadata?.serviceName; + + // serviceName should always exist on fields that have @provides federation data, since + // the only case where serviceName wouldn't exist is on a base type, and in that case, + // the `provides` metadata should never get added to begin with. This should be caught in + // composition work. This kind of error should be validated _before_ composition. + if ( + !serviceName && + fieldFederationMetadata?.provides && + !fieldFederationMetadata?.belongsToValueType + ) + throw Error( + 'Internal Consistency Error: field with provides information does not have service name.', + ); + if (!serviceName) continue; + + const getBaseType = (type: any): any => + isListType(type) || isNonNullType(type) + ? getBaseType(type.ofType) + : type; + const baseType = getBaseType(field.type); + + // field has a @provides directive on it + if (fieldFederationMetadata?.provides) { + if (!isObjectType(baseType)) { + errors.push( + errorWithCode( + 'PROVIDES_NOT_ON_ENTITY', + logServiceAndType(serviceName, typeName, fieldName) + + `uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`, + ), + ); + continue; + } + + const fieldType = types[baseType.name]; + const selectedFieldIsEntity = getFederationMetadata(fieldType)?.keys; + + if (!selectedFieldIsEntity) { + errors.push( + errorWithCode( + 'PROVIDES_NOT_ON_ENTITY', + logServiceAndType(serviceName, typeName, fieldName) + + `uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`, + ), + ); + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts new file mode 100644 index 000000000..02c049f69 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -0,0 +1,57 @@ +import { isObjectType, FieldNode, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * for every @requires, there should be a matching @external + */ +export const requiresFieldsMissingExternal: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + // for each field, if there's a requires on it, check that there's a matching + // @external field, and that the types referenced are from the base type + for (const [fieldName, field] of Object.entries(namedType.getFields())) { + const fieldFederationMetadata = getFederationMetadata(field); + const serviceName = fieldFederationMetadata?.serviceName; + + // serviceName should always exist on fields that have @requires federation data, since + // the only case where serviceName wouldn't exist is on a base type, and in that case, + // the `requires` metadata should never get added to begin with. This should be caught in + // composition work. This kind of error should be validated _before_ composition. + if (!serviceName) continue; + + if (fieldFederationMetadata?.requires) { + const typeFederationMetadata = getFederationMetadata(namedType); + const externalFieldsOnTypeForService = + typeFederationMetadata?.externals?.[serviceName]; + + const selections = fieldFederationMetadata?.requires as FieldNode[]; + for (const selection of selections) { + const foundMatchingExternal = externalFieldsOnTypeForService + ? externalFieldsOnTypeForService.some( + ext => ext.field.name.value === selection.name.value, + ) + : undefined; + if (!foundMatchingExternal) { + errors.push( + errorWithCode( + 'REQUIRES_FIELDS_MISSING_EXTERNAL', + logServiceAndType(serviceName, typeName, fieldName) + + `requires the field \`${selection.name.value}\` to be marked as @external.`, + ), + ); + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts new file mode 100644 index 000000000..401dcd667 --- /dev/null +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -0,0 +1,54 @@ +import { isObjectType, FieldNode, GraphQLError } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { PostCompositionValidator } from '.'; + +/** + * The fields arg in @requires can only reference fields on the base type + */ +export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ + schema, +}) => { + const errors: GraphQLError[] = []; + + const types = schema.getTypeMap(); + for (const [typeName, namedType] of Object.entries(types)) { + // Only object types have fields + if (!isObjectType(namedType)) continue; + + // for each field, if there's a requires on it, check that there's a matching + // @external field, and that the types referenced are from the base type + for (const [fieldName, field] of Object.entries(namedType.getFields())) { + const fieldFederationMetadata = getFederationMetadata(field); + const serviceName = fieldFederationMetadata?.serviceName; + + // serviceName should always exist on fields that have @requires federation data, since + // the only case where serviceName wouldn't exist is on a base type, and in that case, + // the `requires` metadata should never get added to begin with. This should be caught in + // composition work. This kind of error should be validated _before_ composition. + if (!serviceName) continue; + + if (fieldFederationMetadata?.requires) { + const selections = fieldFederationMetadata.requires as FieldNode[]; + for (const selection of selections) { + // check the selections are from the _base_ type (no serviceName) + const matchingFieldOnType = namedType.getFields()[ + selection.name.value + ]; + const typeFederationMetadata = getFederationMetadata(matchingFieldOnType); + + if (typeFederationMetadata?.serviceName) { + errors.push( + errorWithCode( + 'REQUIRES_FIELDS_MISSING_ON_BASE', + logServiceAndType(serviceName, typeName, fieldName) + + `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, + ), + ); + } + } + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts new file mode 100644 index 000000000..cde9552c3 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts @@ -0,0 +1,97 @@ +import gql from 'graphql-tag'; +import { duplicateEnumOrScalar as validateDuplicateEnumOrScalar } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('duplicateEnumOrScalar', () => { + it('does not error with proper enum and scalar usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + shippingDate: Date + type: ProductType + } + + enum ProductType { + BOOK + FURNITURE + } + + extend enum ProductType { + DIGITAL + } + + scalar Date + `, + name: 'serviceA', + }; + + const warnings = validateDuplicateEnumOrScalar(serviceA); + expect(warnings).toEqual([]); + }); + it('errors when there are multiple definitions of the same enum', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + + enum ProductType { + BOOK + FURNITURE + } + + enum ProductType { + DIGITAL + } + `, + name: 'serviceA', + }; + + const warnings = validateDuplicateEnumOrScalar(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "DUPLICATE_ENUM_DEFINITION", + "message": "[serviceA] ProductType -> The enum, \`ProductType\` was defined multiple times in this service. Remove one of the definitions for \`ProductType\`", + }, + ] + `); + }); + + it('errors when there are multiple definitions of the same scalar', () => { + const serviceA = { + typeDefs: gql` + scalar Date + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + deliveryDate: Date + } + + scalar Date + `, + name: 'serviceA', + }; + + const warnings = validateDuplicateEnumOrScalar(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "DUPLICATE_SCALAR_DEFINITION", + "message": "[serviceA] Date -> The scalar, \`Date\` was defined multiple times in this service. Remove one of the definitions for \`Date\`", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts new file mode 100644 index 000000000..6bc91a566 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts @@ -0,0 +1,74 @@ +import gql from 'graphql-tag'; +import { duplicateEnumValue as validateDuplicateEnumValue } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('duplicateEnumValue', () => { + it('does not error with proper enum usage', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + + enum ProductType { + BOOK + FURNITURE + } + + extend enum ProductType { + DIGITAL + } + `, + name: 'serviceA', + }; + + const warnings = validateDuplicateEnumValue(serviceA); + expect(warnings).toEqual([]); + }); + it('errors when there are duplicate enum values in a single service', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + + enum ProductType { + BOOK + FURNITURE + } + + extend enum ProductType { + DIGITAL + BOOK + } + `, + name: 'serviceA', + }; + + const warnings = validateDuplicateEnumValue(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "DUPLICATE_ENUM_VALUE", + "message": "[serviceA] ProductType.BOOK -> The enum, \`ProductType\` has multiple definitions of the \`BOOK\` value.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts new file mode 100644 index 000000000..2b1cad013 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts @@ -0,0 +1,51 @@ +import gql from 'graphql-tag'; +import { externalUsedOnBase as validateExternalUsedOnBase } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('externalUsedOnBase', () => { + it('does not warn when no externals directives are defined', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateExternalUsedOnBase(serviceA); + expect(warnings).toEqual([]); + }); + + it('warns when there is a @external field on a base type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! @external + id: ID! + } + `, + name: 'serviceA', + }; + + const warnings = validateExternalUsedOnBase(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_USED_ON_BASE", + "message": "[serviceA] Product.upc -> Found extraneous @external directive. @external cannot be used on base types.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts new file mode 100644 index 000000000..381169431 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts @@ -0,0 +1,181 @@ +import gql from 'graphql-tag'; +import { keyFieldsMissingExternal as validateKeyFieldsMissingExternal } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('keyFieldsMissingExternal', () => { + it('has no warnings when @key fields reference an @external field', () => { + const serviceA = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! @external + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(0); + }); + + it('has no warnings with correct selection set / nested @external usage', () => { + const serviceA = { + typeDefs: gql` + extend type Car @key(fields: "model { name kit { upc } } year") { + model: Model! @external + year: String! @external + color: String! + } + + extend type Model { + name: String! @external + kit: Kit @external + } + + extend type Kit { + upc: String! @external + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(0); + }); + + it('has no warnings with @deprecated directive usage', () => { + const serviceA = { + typeDefs: gql` + extend type Car @key(fields: "model { name kit { upc } } year") { + model: Model! @external + year: String! @external + color: String! @deprecated(reason: "Use colors instead") + colors: Color! + } + + extend type Model { + name: String! @external + kit: Kit @external + } + + extend type Kit { + upc: String! @external + } + + enum Color { + Red + Blue + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(0); + }); + + it("warns when a @key argument doesn't reference an @external field", () => { + const serviceA = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(1); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_MISSING_EXTERNAL", + "message": "[serviceA] Product -> A @key directive specifies the \`sku\` field which has no matching @external field.", + }, + ] + `); + }); + + it("warns when a @key argument references a field that isn't known", () => { + const serviceA = { + typeDefs: gql` + extend type Product @key(fields: "sku") { + upc: String! @external + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(1); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_MISSING_EXTERNAL", + "message": "[serviceA] Product -> A @key directive specifies a field which is not found in this service. Add a field to this type with @external.", + }, + ] + `); + }); + + it("warns when a @key argument doesn't reference an @external field", () => { + const serviceA = { + typeDefs: gql` + extend type Car @key(fields: "model { name kit { upc } } year") { + model: Model! @external + year: String! @external + } + + extend type Model { + name: String! + kit: Kit + } + + type Kit { + upc: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateKeyFieldsMissingExternal(serviceA); + expect(warnings).toHaveLength(3); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "KEY_FIELDS_MISSING_EXTERNAL", + "message": "[serviceA] Model -> A @key directive specifies the \`name\` field which has no matching @external field.", + }, + Object { + "code": "KEY_FIELDS_MISSING_EXTERNAL", + "message": "[serviceA] Model -> A @key directive specifies the \`kit\` field which has no matching @external field.", + }, + Object { + "code": "KEY_FIELDS_MISSING_EXTERNAL", + "message": "[serviceA] Kit -> A @key directive specifies the \`upc\` field which has no matching @external field.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts new file mode 100644 index 000000000..fc5d42c66 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts @@ -0,0 +1,51 @@ +import gql from 'graphql-tag'; +import { requiresUsedOnBase as validateRequiresUsedOnBase } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('requiresUsedOnBase', () => { + it('does not warn when no requires directives are defined', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "color { id value }") { + sku: String! + upc: String! + color: Color! + } + + type Color { + id: ID! + value: String! + } + `, + name: 'serviceA', + }; + + const warnings = validateRequiresUsedOnBase(serviceA); + expect(warnings).toEqual([]); + }); + + it('warns when there is a @requires field on a base type', () => { + const serviceA = { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: String! + upc: String! @requires(fields: "sku") + id: ID! + } + `, + name: 'serviceA', + }; + + const warnings = validateRequiresUsedOnBase(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "REQUIRES_USED_ON_BASE", + "message": "[serviceA] Product.upc -> Found extraneous @requires directive. @requires cannot be used on base types.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts new file mode 100644 index 000000000..8d03a00e7 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts @@ -0,0 +1,153 @@ +import gql from 'graphql-tag'; +import { reservedFieldUsed as validateReservedFieldUsed } from '..'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('reservedFieldUsed', () => { + it('has no warnings when _service and _entities arent used', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Product { + sku: String + } + `, + name: 'serviceA', + }; + + const warnings = validateReservedFieldUsed(serviceA); + expect(warnings).toEqual([]); + }); + + it('warns when _service or _entities is used at the query root', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + _service: String! + _entities: String! + } + + type Product { + sku: String + } + `, + name: 'serviceA', + }; + + const warnings = validateReservedFieldUsed(serviceA); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "RESERVED_FIELD_USED", + "message": "[serviceA] Query._service -> _service is a field reserved for federation and can't be used at the Query root.", + }, + Object { + "code": "RESERVED_FIELD_USED", + "message": "[serviceA] Query._entities -> _entities is a field reserved for federation and can't be used at the Query root.", + }, + ] + `); + }); + + it('warns when _service or _entities is used in a schema extension', () => { + const schemaDefinition = { + typeDefs: gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + _entities: String! + } + + type Product { + sku: String + } + `, + name: 'schemaDefinition', + }; + + const schemaExtension = { + typeDefs: gql` + extend schema { + query: RootQuery + } + + type RootQuery { + _service: String + product: Product + } + + type Product { + sku: String + } + `, + name: 'schemaExtension', + }; + + const schemaDefinitionWarnings = validateReservedFieldUsed( + schemaDefinition, + ); + const schemaExtensionWarnings = validateReservedFieldUsed(schemaExtension); + + expect(schemaDefinitionWarnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "RESERVED_FIELD_USED", + "message": "[schemaDefinition] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", + }, + ] + `); + expect(schemaExtensionWarnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "RESERVED_FIELD_USED", + "message": "[schemaExtension] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", + }, + ] + `); + }); + + it('warns when reserved fields are used on custom Query types', () => { + const serviceA = { + typeDefs: gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + _service: String + _entities: String + } + + type Product { + sku: String + } + `, + name: 'serviceA', + }; + + const warnings = validateReservedFieldUsed(serviceA); + + expect(warnings).toHaveLength(2); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "RESERVED_FIELD_USED", + "message": "[serviceA] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", + }, + Object { + "code": "RESERVED_FIELD_USED", + "message": "[serviceA] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", + }, + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/preComposition/__tests__/tsconfig.json b/federation-js/src/composition/validate/preComposition/__tests__/tsconfig.json new file mode 100644 index 000000000..9bcedd51a --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [{ "path": "../../../../../" }] +} diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts new file mode 100644 index 000000000..b37520564 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts @@ -0,0 +1,50 @@ +import { visit, GraphQLError } from 'graphql'; +import { ServiceDefinition } from '../../types'; + +import { logServiceAndType, errorWithCode } from '../../utils'; + +export const duplicateEnumOrScalar = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + // keep track of every enum and scalar and error if there are ever duplicates + const enums: string[] = []; + const scalars: string[] = []; + + visit(typeDefs, { + EnumTypeDefinition(definition) { + const name = definition.name.value; + if (enums.includes(name)) { + errors.push( + errorWithCode( + 'DUPLICATE_ENUM_DEFINITION', + logServiceAndType(serviceName, name) + + `The enum, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, + ), + ); + return definition; + } + enums.push(name); + return definition; + }, + ScalarTypeDefinition(definition) { + const name = definition.name.value; + if (scalars.includes(name)) { + errors.push( + errorWithCode( + 'DUPLICATE_SCALAR_DEFINITION', + logServiceAndType(serviceName, name) + + `The scalar, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, + ), + ); + return definition; + } + scalars.push(name); + return definition; + }, + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts new file mode 100644 index 000000000..5e83eb417 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts @@ -0,0 +1,72 @@ +import { visit, GraphQLError } from 'graphql'; +import { ServiceDefinition } from '../../types'; + +import { logServiceAndType, errorWithCode } from '../../utils'; + +export const duplicateEnumValue = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + const enums: { [name: string]: string[] } = {}; + + visit(typeDefs, { + EnumTypeDefinition(definition) { + const name = definition.name.value; + const enumValues = + definition.values && definition.values.map(value => value.name.value); + + if (!enumValues) return definition; + + if (enums[name] && enums[name].length) { + enumValues.map(valueName => { + if (enums[name].includes(valueName)) { + errors.push( + errorWithCode( + 'DUPLICATE_ENUM_VALUE', + logServiceAndType(serviceName, name, valueName) + + `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, + ), + ); + return; + } + enums[name].push(valueName); + }); + } else { + enums[name] = enumValues; + } + + return definition; + }, + EnumTypeExtension(definition) { + const name = definition.name.value; + const enumValues = + definition.values && definition.values.map(value => value.name.value); + + if (!enumValues) return definition; + + if (enums[name] && enums[name].length) { + enumValues.map(valueName => { + if (enums[name].includes(valueName)) { + errors.push( + errorWithCode( + 'DUPLICATE_ENUM_VALUE', + logServiceAndType(serviceName, name, valueName) + + `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, + ), + ); + return; + } + enums[name].push(valueName); + }); + } else { + enums[name] = enumValues; + } + + return definition; + }, + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts new file mode 100644 index 000000000..ddb2b2bd0 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts @@ -0,0 +1,43 @@ +import { visit, GraphQLError } from 'graphql'; +import { ServiceDefinition } from '../../types'; + +import { logServiceAndType, errorWithCode } from '../../utils'; + +/** + * - There are no fields with @external on base type definitions + */ +export const externalUsedOnBase = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + visit(typeDefs, { + ObjectTypeDefinition(typeDefinition) { + if (typeDefinition.fields) { + for (const field of typeDefinition.fields) { + if (field.directives) { + for (const directive of field.directives) { + const name = directive.name.value; + if (name === 'external') { + errors.push( + errorWithCode( + 'EXTERNAL_USED_ON_BASE', + logServiceAndType( + serviceName, + typeDefinition.name.value, + field.name.value, + ) + + `Found extraneous @external directive. @external cannot be used on base types.`, + ), + ); + } + } + } + } + } + }, + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/index.ts b/federation-js/src/composition/validate/preComposition/index.ts new file mode 100644 index 000000000..c2d78e45b --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/index.ts @@ -0,0 +1,6 @@ +export { externalUsedOnBase } from './externalUsedOnBase'; +export { requiresUsedOnBase } from './requiresUsedOnBase'; +export { keyFieldsMissingExternal } from './keyFieldsMissingExternal'; +export { reservedFieldUsed } from './reservedFieldUsed'; +export { duplicateEnumOrScalar } from './duplicateEnumOrScalar'; +export { duplicateEnumValue } from './duplicateEnumValue'; diff --git a/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts new file mode 100644 index 000000000..eb8fc648d --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts @@ -0,0 +1,115 @@ +import { + visit, + visitWithTypeInfo, + TypeInfo, + parse, + GraphQLSchema, + GraphQLError, + specifiedDirectives, +} from 'graphql'; +import { buildSchemaFromSDL } from 'apollo-graphql'; +import { federationDirectives } from '../../../directives'; +import { ServiceDefinition } from '../../types'; +import { + findDirectivesOnTypeOrField, + isStringValueNode, + logServiceAndType, + errorWithCode, + isNotNullOrUndefined +} from '../../utils'; + +/** + * For every @key directive, it must reference a field marked as @external + */ +export const keyFieldsMissingExternal = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + // Build an array that accounts for all key directives on type extensions. + let keyDirectiveInfoOnTypeExtensions: { + typeName: string; + keyArgument: string; + }[] = []; + visit(typeDefs, { + ObjectTypeExtension(node) { + const keyDirectivesOnTypeExtension = findDirectivesOnTypeOrField( + node, + 'key', + ); + + const keyDirectivesInfo = keyDirectivesOnTypeExtension + .map(keyDirective => + keyDirective.arguments && + isStringValueNode(keyDirective.arguments[0].value) + ? { + typeName: node.name.value, + keyArgument: keyDirective.arguments[0].value.value, + } + : null, + ) + .filter(isNotNullOrUndefined); + + keyDirectiveInfoOnTypeExtensions.push(...keyDirectivesInfo); + }, + }); + + // this allows us to build a partial schema + let schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + try { + schema = buildSchemaFromSDL(typeDefs, schema); + } catch (e) { + errors.push(e); + return errors; + } + + const typeInfo = new TypeInfo(schema); + + for (const { typeName, keyArgument } of keyDirectiveInfoOnTypeExtensions) { + const keyDirectiveSelectionSet = parse( + `fragment __generated on ${typeName} { ${keyArgument} }`, + ); + visit( + keyDirectiveSelectionSet, + visitWithTypeInfo(typeInfo, { + Field() { + const fieldDef = typeInfo.getFieldDef(); + const parentType = typeInfo.getParentType(); + if (parentType) { + if (!fieldDef) { + // TODO: find all fields that have @external and suggest them / heursitic match + errors.push( + errorWithCode( + 'KEY_FIELDS_MISSING_EXTERNAL', + logServiceAndType(serviceName, parentType.name) + + `A @key directive specifies a field which is not found in this service. Add a field to this type with @external.`, + ), + ); + return; + } + const externalDirectivesOnField = findDirectivesOnTypeOrField( + fieldDef.astNode, + 'external', + ); + + if (externalDirectivesOnField.length === 0) { + errors.push( + errorWithCode( + 'KEY_FIELDS_MISSING_EXTERNAL', + logServiceAndType(serviceName, parentType.name) + + `A @key directive specifies the \`${fieldDef.name}\` field which has no matching @external field.`, + ), + ); + } + } + }, + }), + ); + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts new file mode 100644 index 000000000..a70a32b35 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts @@ -0,0 +1,43 @@ +import { GraphQLError, visit } from 'graphql'; +import { ServiceDefinition } from '../../types'; + +import { logServiceAndType, errorWithCode } from '../../utils'; + +/** + * - There are no fields with @requires on base type definitions + */ +export const requiresUsedOnBase = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + visit(typeDefs, { + ObjectTypeDefinition(typeDefinition) { + if (typeDefinition.fields) { + for (const field of typeDefinition.fields) { + if (field.directives) { + for (const directive of field.directives) { + const name = directive.name.value; + if (name === 'requires') { + errors.push( + errorWithCode( + 'REQUIRES_USED_ON_BASE', + logServiceAndType( + serviceName, + typeDefinition.name.value, + field.name.value, + ) + + `Found extraneous @requires directive. @requires cannot be used on base types.`, + ), + ); + } + } + } + } + } + }, + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts b/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts new file mode 100644 index 000000000..0434f7bd2 --- /dev/null +++ b/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts @@ -0,0 +1,48 @@ +import { GraphQLError, visit } from 'graphql'; +import { ServiceDefinition } from '../../types'; +import { + logServiceAndType, + errorWithCode, + reservedRootFields +} from '../../utils'; + +/** + * - Schemas should not define the _service or _entitites fields on the query root + */ +export const reservedFieldUsed = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + let rootQueryName = 'Query'; + visit(typeDefs, { + // find the Query type if redefined + OperationTypeDefinition(node) { + if (node.operation === 'query') { + rootQueryName = node.type.name.value; + } + }, + }); + + visit(typeDefs, { + ObjectTypeDefinition(node) { + if (node.name.value === rootQueryName && node.fields) { + for (const field of node.fields) { + const { value: fieldName } = field.name; + if (reservedRootFields.includes(fieldName)) { + errors.push( + errorWithCode( + 'RESERVED_FIELD_USED', + logServiceAndType(serviceName, rootQueryName, fieldName) + + `${fieldName} is a field reserved for federation and can\'t be used at the Query root.`, + ), + ); + } + } + } + }, + }); + + return errors; +}; diff --git a/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts new file mode 100644 index 000000000..69f46733e --- /dev/null +++ b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts @@ -0,0 +1,170 @@ +import gql from 'graphql-tag'; +import { rootFieldUsed as validateRootFieldUsed } from '../'; +import { graphqlErrorSerializer } from '../../../../snapshotSerializers'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); + +describe('rootFieldUsed', () => { + it('has no warnings when no schema definition or extension is provided', () => { + const serviceA = { + typeDefs: gql` + type Query { + product: Product + } + + type Product { + sku: String + } + `, + name: 'serviceA', + }; + + const warnings = validateRootFieldUsed(serviceA); + expect(warnings).toHaveLength(0); + }); + + it('has no warnings when a schema definition / extension is provided, when no default root operation type names are used', () => { + const schemaDefinition = { + typeDefs: gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product { + sku: String + } + `, + name: 'schemaDefinition', + }; + + const schemaExtension = { + typeDefs: gql` + extend schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product { + sku: String + } + `, + name: 'schemaExtension', + }; + + const schemaDefinitionWarnings = validateRootFieldUsed(schemaDefinition); + const schemaExtensionWarnings = validateRootFieldUsed(schemaExtension); + + expect(schemaDefinitionWarnings).toEqual([]); + expect(schemaExtensionWarnings).toEqual([]); + }); + + it('warns when a schema definition / extension is provided, as well as a default root type or extension', () => { + const serviceA = { + typeDefs: gql` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product { + sku: String + } + + type Query { + invalidUseOfQuery: Boolean + } + `, + name: 'serviceA', + }; + + const warnings = validateRootFieldUsed(serviceA); + + expect(warnings).toHaveLength(1); + expect(warnings[0].extensions.code).toEqual('ROOT_QUERY_USED'); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "ROOT_QUERY_USED", + "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", + }, + ] + `); + }); + + it('warns against using default operation type names (Query, Mutation, Subscription) when a non-default operation type name is provided in the schema definition', () => { + const serviceA = { + typeDefs: gql` + schema { + mutation: RootMutation + } + + type RootMutation { + updateProduct(sku: ID!): Product + } + + type Mutation { + invalidUseOfMutation: Boolean + } + `, + name: 'serviceA', + }; + + const warnings = validateRootFieldUsed(serviceA); + + expect(warnings).toHaveLength(1); + expect(warnings).toMatchInlineSnapshot(` + Array [ + Object { + "code": "ROOT_MUTATION_USED", + "message": "[serviceA] Mutation -> Found invalid use of default root operation name \`Mutation\`. \`Mutation\` is disallowed when \`Schema.mutation\` is set to a type other than \`Mutation\`.", + }, + ] + `); + }); + + it("doesn't warn against using default operation type names when no schema definition is provided", () => { + const serviceA = { + typeDefs: gql` + type Query { + validUseOfQuery: Boolean + } + `, + name: 'serviceA', + }; + + const warnings = validateRootFieldUsed(serviceA); + expect(warnings).toHaveLength(0); + }); + + it("doesn't warn against using default operation type names when a schema is defined", () => { + const serviceA = { + typeDefs: gql` + schema { + mutation: Mutation + } + + type Query { + validUseOfQuery: Boolean + } + + type Mutation { + validUseOfMutation: Product + } + `, + name: 'serviceA', + }; + + const warnings = validateRootFieldUsed(serviceA); + expect(warnings).toHaveLength(0); + }); +}); diff --git a/federation-js/src/composition/validate/preNormalization/index.ts b/federation-js/src/composition/validate/preNormalization/index.ts new file mode 100644 index 000000000..7ff717994 --- /dev/null +++ b/federation-js/src/composition/validate/preNormalization/index.ts @@ -0,0 +1 @@ +export { rootFieldUsed } from './rootFieldUsed'; diff --git a/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts b/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts new file mode 100644 index 000000000..1e6bdc139 --- /dev/null +++ b/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts @@ -0,0 +1,81 @@ +import { + GraphQLError, + visit, + ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, +} from 'graphql'; +import { ServiceDefinition, DefaultRootOperationTypeName } from '../../types'; +import { + logServiceAndType, + errorWithCode, + defaultRootOperationNameLookup +} from '../../utils'; + +/** + * - When a schema definition or extension is provided, warn user against using + * default root operation type names for types or extensions + * (Query, Mutation, Subscription) + */ +export const rootFieldUsed = ({ + name: serviceName, + typeDefs, +}: ServiceDefinition) => { + const errors: GraphQLError[] = []; + + // Array of default root operation names + const defaultRootOperationNames = Object.values( + defaultRootOperationNameLookup, + ); + + const disallowedTypeNames: { + [key in DefaultRootOperationTypeName]?: boolean; + } = {}; + + let hasSchemaDefinitionOrExtension = false; + visit(typeDefs, { + OperationTypeDefinition(node) { + // If we find at least one root operation type definition, we know the user has + // specified either a schema definition or extension. + hasSchemaDefinitionOrExtension = true; + + if ( + !defaultRootOperationNames.includes(node.type.name + .value as DefaultRootOperationTypeName) + ) { + disallowedTypeNames[ + defaultRootOperationNameLookup[node.operation] + ] = true; + } + }, + }); + + // If a schema or schema extension is defined, we need to warn for each improper + // usage of default root operation names. The conditions for an improper usage are: + // 1. root operation type is defined as a non-default name (i.e. query: RootQuery) + // 2. the respective default operation type name is used as a regular type + if (hasSchemaDefinitionOrExtension) { + visit(typeDefs, { + ObjectTypeDefinition: visitType, + ObjectTypeExtension: visitType, + }); + + function visitType( + node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, + ) { + if ( + disallowedTypeNames[node.name.value as DefaultRootOperationTypeName] + ) { + const rootOperationName = node.name.value; + errors.push( + errorWithCode( + `ROOT_${rootOperationName.toUpperCase()}_USED`, + logServiceAndType(serviceName, rootOperationName) + + `Found invalid use of default root operation name \`${rootOperationName}\`. \`${rootOperationName}\` is disallowed when \`Schema.${rootOperationName.toLowerCase()}\` is set to a type other than \`${rootOperationName}\`.`, + ), + ); + } + } + } + + return errors; +}; diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts new file mode 100644 index 000000000..77b86452c --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts @@ -0,0 +1,205 @@ +import { + GraphQLEnumType, + Kind, + DocumentNode, + validate, + GraphQLSchema, + specifiedDirectives, +} from 'graphql'; +import { validateSDL } from 'graphql/validation/validate'; +import gql from 'graphql-tag'; +import { composeServices, buildMapsFromServiceList } from '../../../compose'; +import { + astSerializer, + typeSerializer, + selectionSetSerializer, +} from '../../../../snapshotSerializers'; +import { normalizeTypeDefs } from '../../../normalize'; +import federationDirectives from '../../../../directives'; +import { ServiceDefinition } from '../../../types'; +import { MatchingEnums } from '../matchingEnums'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(typeSerializer); +expect.addSnapshotSerializer(selectionSetSerializer); + +// simulate the first half of the composition process +const createDefinitionsDocumentForServices = ( + serviceList: ServiceDefinition[], +): DocumentNode => { + const { typeDefinitionsMap } = buildMapsFromServiceList(serviceList); + return { + kind: Kind.DOCUMENT, + definitions: Object.values(typeDefinitionsMap).flat(), + }; +}; + +describe('matchingEnums', () => { + let schema: GraphQLSchema; + + // create a blank schema for each test + beforeEach(() => { + schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + }); + + it('does not error with matching enums across services', () => { + const serviceList = [ + { + typeDefs: gql` + enum ProductCategory { + BED + BATH + } + `, + name: 'serviceA', + }, + + { + typeDefs: gql` + enum ProductCategory { + BED + BATH + } + `, + name: 'serviceB', + }, + ]; + + const definitionsDocument = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); + expect(errors).toHaveLength(0); + }); + + it('errors when enums in separate services dont match', () => { + const serviceList = [ + { + typeDefs: gql` + enum ProductCategory { + BED + BATH + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + enum ProductCategory { + BEYOND + } + `, + name: 'serviceB', + }, + ]; + + const definitionsDocument = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: The \`ProductCategory\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB]], + ] + `); + }); + + it('errors when enums in separate services dont match', () => { + const serviceList = [ + { + typeDefs: gql` + type Query { + products: [Product]! + } + + type Product @key(fields: "sku") { + sku: String! + upc: String! + type: ProductType + } + + enum ProductType { + BOOK + FURNITURE + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + enum ProductType { + FURNITURE + BOOK + DIGITAL + } + `, + name: 'serviceB', + }, + { + typeDefs: gql` + enum ProductType { + FURNITURE + BOOK + DIGITAL + } + `, + name: 'serviceC', + }, + ]; + + const definitionsDocument = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: The \`ProductType\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB, serviceC]], + ] + `); + }); + + it('errors when an enum name is defined as another type in a service', () => { + const serviceList = [ + { + typeDefs: gql` + enum ProductType { + BOOK + FURNITURE + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type ProductType { + id: String + } + `, + name: 'serviceB', + }, + { + typeDefs: gql` + enum ProductType { + FURNITURE + BOOK + DIGITAL + } + `, + name: 'serviceC', + }, + ]; + + const definitionsDocument = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: [serviceA] ProductType -> ProductType is an enum in [serviceA, serviceC], but not in [serviceB]], + ] + `); + }); +}); diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts new file mode 100644 index 000000000..7a3ec2b7e --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts @@ -0,0 +1,132 @@ +import { + GraphQLSchema, + specifiedDirectives, + Kind, + DocumentNode, +} from 'graphql'; +import { validateSDL } from 'graphql/validation/validate'; +import gql from 'graphql-tag'; +import { + typeSerializer, + graphqlErrorSerializer, +} from '../../../../snapshotSerializers'; +import { UniqueUnionTypes } from '..'; +import { ServiceDefinition } from '../../../types'; +import { buildMapsFromServiceList } from '../../../compose'; +import federationDirectives from '../../../../directives'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); +expect.addSnapshotSerializer(typeSerializer); + +function createDocumentsForServices( + serviceList: ServiceDefinition[], +): DocumentNode[] { + const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( + serviceList, + ); + return [ + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeDefinitionsMap).flat(), + }, + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeExtensionsMap).flat(), + }, + ]; +} + +describe('MatchingUnions', () => { + let schema: GraphQLSchema; + + // create a blank schema for each test + beforeEach(() => { + schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + }); + + it('enforces unique union names on non-identical union types', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + union ProductOrError = Product | Error + + type Error { + code: Int! + message: String! + } + + type Product @key(fields: "sku") { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + union ProductOrError = Product + + type Error { + code: Int! + message: String! + } + + extend type Product @key(fields: "sku") { + sku: ID! @external + colors: [String] + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [UniqueUnionTypes]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", + "message": "[serviceA] ProductOrError -> The union \`ProductOrError\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The type Error is mismatched.", + } + `); + }); + + it('permits duplicate union names for identical union types', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + union ProductOrError = Product | Error + + type Error { + code: Int! + message: String! + } + + type Product @key(fields: "sku") { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + union ProductOrError = Product | Error + + type Error { + code: Int! + message: String! + } + + type Product @key(fields: "sku") { + sku: ID! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [UniqueUnionTypes]); + expect(errors).toHaveLength(0); + }); +}); diff --git a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts new file mode 100644 index 000000000..7be01981c --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts @@ -0,0 +1,198 @@ +import { + Kind, + DocumentNode, + GraphQLSchema, + specifiedDirectives, + extendSchema, +} from 'graphql'; +import { validateSDL } from 'graphql/validation/validate'; +import gql from 'graphql-tag'; +import { buildMapsFromServiceList } from '../../../compose'; +import { + typeSerializer, + graphqlErrorSerializer, +} from '../../../../snapshotSerializers'; +import federationDirectives from '../../../../directives'; +import { ServiceDefinition } from '../../../types'; +import { PossibleTypeExtensions } from '../possibleTypeExtensions'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); +expect.addSnapshotSerializer(typeSerializer); + +// simulate the first half of the composition process +const createDefinitionsDocumentForServices = ( + serviceList: ServiceDefinition[], +): { + definitions: DocumentNode; + extensions: DocumentNode; +} => { + const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( + serviceList, + ); + return { + definitions: { + kind: Kind.DOCUMENT, + definitions: Object.values(typeDefinitionsMap).flat(), + }, + extensions: { + kind: Kind.DOCUMENT, + definitions: Object.values(typeExtensionsMap).flat(), + }, + }; +}; + +describe('PossibleTypeExtensionsType', () => { + let schema: GraphQLSchema; + + // create a blank schema for each test + beforeEach(() => { + schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + }); + + it('does not error with matching enums across services', () => { + const serviceList = [ + { + typeDefs: gql` + extend type Product { + sku: ID + } + `, + name: 'serviceA', + }, + + { + typeDefs: gql` + type Product { + id: ID! + } + `, + name: 'serviceB', + }, + ]; + + const { definitions, extensions } = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); + expect(errors).toHaveLength(0); + }); + + it('errors when there is an extension with no base', () => { + const serviceList = [ + { + typeDefs: gql` + extend type Product { + id: ID! + } + `, + name: 'serviceA', + }, + ]; + + const { definitions, extensions } = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); + + expect(errors).toHaveLength(1); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTENSION_WITH_NO_BASE", + "message": "[serviceA] Product -> \`Product\` is an extension type, but \`Product\` is not defined in any service", + }, + ] + `); + }); + + it('errors when trying to extend a type with a different `Kind`', () => { + const serviceList = [ + { + typeDefs: gql` + extend type Product { + sku: ID + } + `, + name: 'serviceA', + }, + + { + typeDefs: gql` + input Product { + id: ID! + } + `, + name: 'serviceB', + }, + ]; + + const { definitions, extensions } = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTENSION_OF_WRONG_KIND", + "message": "[serviceA] Product -> \`Product\` was originally defined as a InputObjectTypeDefinition and can only be extended by a InputObjectTypeExtension. serviceA defines Product as a ObjectTypeExtension", + }, + ] + `); + }); + + it('does not error', () => { + const serviceList = [ + { + typeDefs: gql` + extend interface Product { + name: String + } + extend type Book implements Product { + sku: ID! + name: String + } + `, + name: 'serviceA', + }, + + { + typeDefs: gql` + type Book { + id: ID! + } + + interface Product { + sku: ID! + } + `, + name: 'serviceB', + }, + ]; + + const { definitions, extensions } = createDefinitionsDocumentForServices( + serviceList, + ); + const errors = validateSDL(definitions, schema, [PossibleTypeExtensions]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); + schema = extendSchema(schema, extensions, { assumeValidSDL: true }); + + expect(schema.getType('Book')).toMatchInlineSnapshot(` + type Book implements Product { + id: ID! + sku: ID! + name: String + } + `); + expect(errors).toHaveLength(0); + }); +}); diff --git a/federation-js/src/composition/validate/sdl/__tests__/tsconfig.json b/federation-js/src/composition/validate/sdl/__tests__/tsconfig.json new file mode 100644 index 000000000..9bcedd51a --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [{ "path": "../../../../../" }] +} diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts new file mode 100644 index 000000000..cf56c59a8 --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueFieldDefinitionNames.test.ts @@ -0,0 +1,268 @@ +import { + GraphQLSchema, + specifiedDirectives, + DocumentNode, + Kind, + extendSchema, +} from 'graphql'; +import { validateSDL } from 'graphql/validation/validate'; +import gql from 'graphql-tag'; +import { typeSerializer } from '../../../../snapshotSerializers'; +import { buildMapsFromServiceList } from '../../../compose'; +import federationDirectives from '../../../../directives'; +import { UniqueFieldDefinitionNames } from '..'; +import { ServiceDefinition } from '../../../types'; + +expect.addSnapshotSerializer(typeSerializer); + +// simulate the first half of the composition process +function createDocumentsForServices( + serviceList: ServiceDefinition[], +): DocumentNode[] { + const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( + serviceList, + ); + return [ + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeDefinitionsMap).flat(), + }, + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeExtensionsMap).flat(), + }, + ]; +} + +describe('UniqueFieldDefinitionNames', () => { + let schema: GraphQLSchema; + + // create a blank schema for each test + beforeEach(() => { + schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + }); + + describe('enforces unique field names for', () => { + it('object type definitions', () => { + const [definitions, extensions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + extend type Product { + sku: Int! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + schema = extendSchema(schema, definitions, { + assumeValidSDL: true, + }); + + errors.push( + ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), + ); + + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'Field "Product.sku" already exists in the schema.', + ); + }); + + it('interface definitions', () => { + const [definitions, extensions] = createDocumentsForServices([ + { + typeDefs: gql` + interface Product { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + extend interface Product { + sku: String! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push( + ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'Field "Product.sku" already exists in the schema.', + ); + }); + + it('input object definitions', () => { + const [definitions, extensions] = createDocumentsForServices([ + { + typeDefs: gql` + input Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + extend input Product { + sku: String! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + schema = extendSchema(schema, definitions, { assumeValidSDL: true }); + errors.push( + ...validateSDL(extensions, schema, [UniqueFieldDefinitionNames]), + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'Field "Product.sku" already exists in the schema.', + ); + }); + }); + + describe('permits duplicate field names for', () => { + it('value types (identical object types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + color: String + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: ID! + color: String + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + expect(errors).toHaveLength(0); + }); + + it('value types (identical interface types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + interface Product { + sku: ID! + color: String + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + interface Product { + sku: ID! + color: String + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + expect(errors).toHaveLength(0); + }); + + it('value types (identical input types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + input Product { + sku: ID! + color: String + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + input Product { + sku: ID! + color: String + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + expect(errors).toHaveLength(0); + }); + + it('object type definitions (non-identical, value types with type mismatch)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + color: String + quantity: Int + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: String! + color: String + quantity: Int! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueFieldDefinitionNames, + ]); + expect(errors).toHaveLength(0); + }); + }); +}); diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts new file mode 100644 index 000000000..a56c6c6ae --- /dev/null +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -0,0 +1,541 @@ +import { + GraphQLSchema, + specifiedDirectives, + Kind, + DocumentNode, +} from 'graphql'; +import { validateSDL } from 'graphql/validation/validate'; +import gql from 'graphql-tag'; +import { + typeSerializer, + graphqlErrorSerializer, +} from '../../../../snapshotSerializers'; +import federationDirectives from '../../../../directives'; +import { UniqueTypeNamesWithFields } from '..'; +import { ServiceDefinition } from '../../../types'; +import { buildMapsFromServiceList } from '../../../compose'; + +expect.addSnapshotSerializer(graphqlErrorSerializer); +expect.addSnapshotSerializer(typeSerializer); + +function createDocumentsForServices( + serviceList: ServiceDefinition[], +): DocumentNode[] { + const { typeDefinitionsMap, typeExtensionsMap } = buildMapsFromServiceList( + serviceList, + ); + return [ + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeDefinitionsMap).flat(), + }, + { + kind: Kind.DOCUMENT, + definitions: Object.values(typeExtensionsMap).flat(), + }, + ]; +} + +describe('UniqueTypeNamesWithFields', () => { + let schema: GraphQLSchema; + + // create a blank schema for each test + beforeEach(() => { + schema = new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }); + }); + + describe('enforces unique type names for', () => { + it('object type definitions (non-identical, non-value types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + color: String! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'There can be only one type named "Product".', + ); + }); + + it('object type definitions (non-identical, value types with type mismatch)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + color: String + quantity: Int + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: String! + color: String + quantity: Int! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(2); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "message": "[serviceA] Product.sku -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.sku\` as a ID! and String! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", + }, + Object { + "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "message": "[serviceA] Product.quantity -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.quantity\` as a Int and Int! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", + }, + ] + `); + }); + + it('object type definitions (overlapping fields, but non-value types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID! + color: String + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: ID! + blah: Int! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'There can be only one type named "Product".', + ); + }); + + it('interface definitions', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + interface Product { + sku: ID! + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + interface Product { + color: String! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'There can be only one type named "Product".', + ); + }); + + it('input definitions', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + input Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + input Product { + color: String! + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0].message).toMatch( + 'There can be only one type named "Product".', + ); + }); + }); + + describe('permits duplicate type names for', () => { + it('scalar types', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + scalar JSON + `, + name: 'serviceA', + }, + { + typeDefs: gql` + scalar JSON + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(0); + }); + + it('enum types (congruency enforced in other validations)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + enum Category { + Furniture + Supplies + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + enum Category { + Things + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(0); + }); + + it('input types', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + input Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + input Product { + sku: ID + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(0); + }); + + it('value types (non-entity type definitions that are identical)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: ID + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(0); + }); + }); + + describe('edge cases', () => { + it('value types must be of the same kind', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + input Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: ID + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_KIND_MISMATCH", + "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InputObjectTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", + } + `); + }); + + it('value types must be of the same kind (scalar)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + scalar DateTime + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type DateTime { + day: Int + formatted: String + # ... + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_KIND_MISMATCH", + "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`ScalarTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", + } + `); + }); + + it('value types must be of the same kind (union)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + union DateTime = Date | Time + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type DateTime { + day: Int + formatted: String + # ... + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_KIND_MISMATCH", + "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`UnionTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", + } + `); + }); + + it('value types must be of the same kind (enum)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + enum DateTime { + DATE + TIME + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type DateTime { + day: Int + formatted: String + # ... + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_KIND_MISMATCH", + "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`EnumTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", + } + `); + }); + + it('value types cannot be entities (part 1)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product { + sku: ID + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_NO_ENTITY", + "message": "[serviceA] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", + } + `); + }); + + it('value types cannot be entities (part 2)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: ID + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchInlineSnapshot(` + Object { + "code": "VALUE_TYPE_NO_ENTITY", + "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", + } + `); + }); + + it('no false positives for properly formed entities (that look like value types)', () => { + const [definitions] = createDocumentsForServices([ + { + typeDefs: gql` + type Product @key(fields: "sku") { + sku: ID + } + `, + name: 'serviceA', + }, + { + typeDefs: gql` + extend type Product @key(fields: "sku") { + sku: ID @external + } + `, + name: 'serviceB', + }, + ]); + + const errors = validateSDL(definitions, schema, [ + UniqueTypeNamesWithFields, + ]); + expect(errors).toHaveLength(0); + }); + }); +}); diff --git a/federation-js/src/composition/validate/sdl/index.ts b/federation-js/src/composition/validate/sdl/index.ts new file mode 100644 index 000000000..5a3ed5762 --- /dev/null +++ b/federation-js/src/composition/validate/sdl/index.ts @@ -0,0 +1,5 @@ +export { UniqueTypeNamesWithFields } from './uniqueTypeNamesWithFields'; +export { MatchingEnums } from './matchingEnums'; +export { PossibleTypeExtensions } from './possibleTypeExtensions'; +export { UniqueFieldDefinitionNames } from './uniqueFieldDefinitionNames'; +export { UniqueUnionTypes } from './matchingUnions'; diff --git a/federation-js/src/composition/validate/sdl/matchingEnums.ts b/federation-js/src/composition/validate/sdl/matchingEnums.ts new file mode 100644 index 000000000..bb162963a --- /dev/null +++ b/federation-js/src/composition/validate/sdl/matchingEnums.ts @@ -0,0 +1,123 @@ +import { SDLValidationContext } from 'graphql/validation/ValidationContext'; +import { + ASTVisitor, + Kind, + EnumTypeDefinitionNode, + EnumValueDefinitionNode, + TypeDefinitionNode, +} from 'graphql'; +import { errorWithCode, logServiceAndType } from '../../utils'; +import { isString } from 'util'; + +function isEnumDefinition(node: TypeDefinitionNode) { + return node.kind === Kind.ENUM_TYPE_DEFINITION; +} + +type TypeToDefinitionsMap = { + [typeNems: string]: TypeDefinitionNode[]; +}; + +export function MatchingEnums(context: SDLValidationContext): ASTVisitor { + const { definitions } = context.getDocument(); + + // group all definitions by name + // { MyTypeName: [{ serviceName: "A", name: {...}}]} + let definitionsByName: { + [typeName: string]: TypeDefinitionNode[]; + } = (definitions as TypeDefinitionNode[]).reduce( + (typeToDefinitionsMap: TypeToDefinitionsMap, node) => { + const name = node.name.value; + if (typeToDefinitionsMap[name]) { + typeToDefinitionsMap[name].push(node); + } else { + typeToDefinitionsMap[name] = [node]; + } + return typeToDefinitionsMap; + }, + {}, + ); + + // map over each group of definitions. + for (const [name, definitions] of Object.entries(definitionsByName)) { + // if every definition in the list is an enum, we don't need to error about type, + // but we do need to check to make sure every service has the same enum values + if (definitions.every(isEnumDefinition)) { + // a simple list of services to enum values for a given enum + // [{ serviceName: "serviceA", values: ["FURNITURE", "BOOK"] }] + let simpleEnumDefs: Array<{ serviceName: string; values: string[] }> = []; + + // build the simpleEnumDefs list + for (const { + values, + serviceName, + } of definitions as EnumTypeDefinitionNode[]) { + if (serviceName && values) + simpleEnumDefs.push({ + serviceName, + values: values.map( + (enumValue: EnumValueDefinitionNode) => enumValue.name.value, + ), + }); + } + + // values need to be in order to build the matchingEnumGroups + for (const definition of simpleEnumDefs) { + definition.values = definition.values.sort(); + } + + // groups of services with matching values, keyed by enum values + // like {"FURNITURE,BOOK": ["ServiceA", "ServiceB"], "FURNITURE,DIGITAL": ["serviceC"]} + let matchingEnumGroups: { [values: string]: string[] } = {}; + + // build matchingEnumDefs + for (const definition of simpleEnumDefs) { + const key = definition.values.join(); + if (matchingEnumGroups[key]) { + matchingEnumGroups[key].push(definition.serviceName); + } else { + matchingEnumGroups[key] = [definition.serviceName]; + } + } + + if (Object.keys(matchingEnumGroups).length > 1) { + context.reportError( + errorWithCode( + 'ENUM_MISMATCH', + `The \`${name}\` enum does not have identical values in all services. Groups of services with identical values are: ${Object.values( + matchingEnumGroups, + ) + .map(serviceNames => `[${serviceNames.join(', ')}]`) + .join(', ')}`, + ), + ); + } + } else if (definitions.some(isEnumDefinition)) { + // if only SOME definitions in the list are enums, we need to error + + // first, find the services, where the defs ARE enums + const servicesWithEnum = definitions + .filter(isEnumDefinition) + .map(definition => definition.serviceName) + .filter(isString); + + // find the services where the def isn't an enum + const servicesWithoutEnum = definitions + .filter(d => !isEnumDefinition(d)) + .map(d => d.serviceName) + .filter(isString); + + context.reportError( + errorWithCode( + 'ENUM_MISMATCH_TYPE', + logServiceAndType(servicesWithEnum[0], name) + + `${name} is an enum in [${servicesWithEnum.join( + ', ', + )}], but not in [${servicesWithoutEnum.join(', ')}]`, + ), + ); + } + } + + // we don't need any visitors for this validation rule + return {}; +} diff --git a/federation-js/src/composition/validate/sdl/matchingUnions.ts b/federation-js/src/composition/validate/sdl/matchingUnions.ts new file mode 100644 index 000000000..7a74fb833 --- /dev/null +++ b/federation-js/src/composition/validate/sdl/matchingUnions.ts @@ -0,0 +1,89 @@ +import { GraphQLError, ASTVisitor, UnionTypeDefinitionNode } from 'graphql'; +import { SDLValidationContext } from 'graphql/validation/ValidationContext'; +import xorBy from 'lodash.xorby'; +import { Maybe } from '../../types'; +import { errorWithCode, logServiceAndType } from '../../utils'; +import { + existedTypeNameMessage, + duplicateTypeNameMessage, +} from './uniqueTypeNamesWithFields'; + +/** + * Unique type names + * A GraphQL document is only valid if all defined types have unique names. + * Modified to allow duplicate enum and scalar names + */ +export function UniqueUnionTypes(context: SDLValidationContext): ASTVisitor { + const knownTypes: { + [typeName: string]: UnionTypeDefinitionNode; + } = Object.create(null); + const schema = context.getSchema(); + + return { + UnionTypeDefinition: validateUnionTypes, + }; + + function validateUnionTypes(node: UnionTypeDefinitionNode) { + const typeName = node.name.value; + const typeFromSchema = schema && schema.getType(typeName); + const typeNodeFromSchema = + typeFromSchema && + (typeFromSchema.astNode as Maybe); + + const typeNodeFromDefs = knownTypes[typeName]; + const duplicateTypeNode = typeNodeFromSchema || typeNodeFromDefs; + + // Exception for identical union types + if (duplicateTypeNode) { + const unionDiff = xorBy( + node.types, + duplicateTypeNode.types, + 'name.value', + ); + + const diffLength = unionDiff.length; + if (diffLength > 0) { + context.reportError( + errorWithCode( + 'VALUE_TYPE_UNION_TYPES_MISMATCH', + `${logServiceAndType( + duplicateTypeNode.serviceName!, + typeName, + )}The union \`${typeName}\` is defined in services \`${ + duplicateTypeNode.serviceName + }\` and \`${ + node.serviceName + }\`, however their types do not match. Union types with the same name must also consist of identical types. The type${ + diffLength > 1 ? 's' : '' + } ${unionDiff.map(diffEntry => diffEntry.name.value).join(', ')} ${ + diffLength > 1 ? 'are' : 'is' + } mismatched.`, + [node, duplicateTypeNode], + ), + ); + } + + return false; + } + + if (typeFromSchema) { + context.reportError( + new GraphQLError(existedTypeNameMessage(typeName), node.name), + ); + return; + } + + if (knownTypes[typeName]) { + context.reportError( + new GraphQLError(duplicateTypeNameMessage(typeName), [ + knownTypes[typeName], + node.name, + ]), + ); + } else { + knownTypes[typeName] = node; + } + + return false; + } +} diff --git a/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts new file mode 100644 index 000000000..f7e37850a --- /dev/null +++ b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts @@ -0,0 +1,129 @@ +import { SDLValidationContext } from 'graphql/validation/ValidationContext'; +import { + ASTVisitor, + isObjectType, + isScalarType, + isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, + Kind, + isTypeDefinitionNode, + ObjectTypeExtensionNode, + InterfaceTypeExtensionNode, + GraphQLNamedType, +} from 'graphql'; +import { + errorWithCode, + logServiceAndType, + defKindToExtKind, +} from '../../utils'; + +type FederatedExtensionNode = ( + | ObjectTypeExtensionNode + | InterfaceTypeExtensionNode) & { + serviceName?: string | null; +}; + +// This is a variant of the PossibleTypeExtensions validator in graphql-js. +// it was modified to only check object/interface extensions. A custom error +// message was also added. +// original here: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleTypeExtensions.js +export function PossibleTypeExtensions( + context: SDLValidationContext, +): ASTVisitor { + const schema = context.getSchema(); + const definedTypes = Object.create(null); + + for (const def of context.getDocument().definitions) { + if (isTypeDefinitionNode(def)) { + definedTypes[def.name.value] = def; + } + } + + const checkExtension = (node: FederatedExtensionNode) => { + const typeName = node.name.value; + const defNode = definedTypes[typeName]; + const existingType = schema && schema.getType(typeName); + + const serviceName = node.serviceName; + if (!serviceName) return; + + if (defNode) { + const expectedKind = defKindToExtKind[defNode.kind]; + const baseKind = defNode.kind; + if (expectedKind !== node.kind) { + context.reportError( + errorWithCode( + 'EXTENSION_OF_WRONG_KIND', + logServiceAndType(serviceName, typeName) + + `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, + ), + ); + } + } else if (existingType) { + const expectedKind = typeToExtKind(existingType); + const baseKind = typeToKind(existingType); + if (expectedKind !== node.kind) { + context.reportError( + errorWithCode( + 'EXTENSION_OF_WRONG_KIND', + logServiceAndType(serviceName, typeName) + + `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, + ), + ); + } + } else { + context.reportError( + errorWithCode( + 'EXTENSION_WITH_NO_BASE', + logServiceAndType(serviceName, typeName) + + `\`${typeName}\` is an extension type, but \`${typeName}\` is not defined in any service`, + ), + ); + } + }; + + return { + ObjectTypeExtension: checkExtension, + InterfaceTypeExtension: checkExtension, + }; +} + +// These following utility functions/objects are part of the +// PossibleTypeExtensions validations in graphql-js, but not exported. +// https://github.com/graphql/graphql-js/blob/d8c1dfdc9dbbdef2400363cb0748d50cbeef39a8/src/validation/rules/PossibleTypeExtensions.js#L110 +function typeToExtKind(type: GraphQLNamedType) { + if (isScalarType(type)) { + return Kind.SCALAR_TYPE_EXTENSION; + } else if (isObjectType(type)) { + return Kind.OBJECT_TYPE_EXTENSION; + } else if (isInterfaceType(type)) { + return Kind.INTERFACE_TYPE_EXTENSION; + } else if (isUnionType(type)) { + return Kind.UNION_TYPE_EXTENSION; + } else if (isEnumType(type)) { + return Kind.ENUM_TYPE_EXTENSION; + } else if (isInputObjectType(type)) { + return Kind.INPUT_OBJECT_TYPE_EXTENSION; + } + return null; +} + +// this function is purely for printing out the `Kind` of the base type def. +function typeToKind(type: GraphQLNamedType) { + if (isScalarType(type)) { + return Kind.SCALAR_TYPE_DEFINITION; + } else if (isObjectType(type)) { + return Kind.OBJECT_TYPE_DEFINITION; + } else if (isInterfaceType(type)) { + return Kind.INTERFACE_TYPE_DEFINITION; + } else if (isUnionType(type)) { + return Kind.UNION_TYPE_DEFINITION; + } else if (isEnumType(type)) { + return Kind.ENUM_TYPE_DEFINITION; + } else if (isInputObjectType(type)) { + return Kind.INPUT_OBJECT_TYPE_DEFINITION; + } + return null; +} diff --git a/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts b/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts new file mode 100644 index 000000000..449174184 --- /dev/null +++ b/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts @@ -0,0 +1,208 @@ +import { + ASTVisitor, + NameNode, + GraphQLError, + InputObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + InputObjectTypeExtensionNode, + InterfaceTypeExtensionNode, + ObjectTypeExtensionNode, + GraphQLNamedType, + isObjectType, + isInterfaceType, + isInputObjectType, +} from 'graphql'; +import { SDLValidationContext } from 'graphql/validation/ValidationContext'; +import { TypeMap } from 'graphql/type/schema'; +import { Maybe } from '../../types'; +import { diffTypeNodes, logServiceAndType } from '../../utils'; + +type TypeNodeWithFields = TypeDefinitionWithFields | TypeExtensionWithFields; + +type TypeDefinitionWithFields = + | InputObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | ObjectTypeDefinitionNode; + +type TypeExtensionWithFields = + | InputObjectTypeExtensionNode + | InterfaceTypeExtensionNode + | ObjectTypeExtensionNode; + +export function duplicateFieldDefinitionNameMessage( + typeName: string, + fieldName: string, +): string { + return `Field "${typeName}.${fieldName}" can only be defined once.`; +} + +export function existedFieldDefinitionNameMessage( + typeName: string, + fieldName: string, + serviceName: string, +): string { + return `${logServiceAndType( + serviceName, + typeName, + fieldName, + )}Field "${typeName}.${fieldName}" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.`; +} + +/** + * Unique field definition names + * + * A GraphQL complex type is only valid if all its fields are uniquely named. + * Modified to permit duplicate field names on value types. + */ +export function UniqueFieldDefinitionNames( + context: SDLValidationContext, +): ASTVisitor { + const schema = context.getSchema(); + const existingTypeMap: TypeMap = schema + ? schema.getTypeMap() + : Object.create(null); + interface FieldToNameNodeMap { + [fieldName: string]: NameNode; + } + const knownFieldNames: { + [typeName: string]: FieldToNameNodeMap; + } = Object.create(null); + + const possibleValueTypes: { + [key: string]: TypeNodeWithFields | undefined; + } = Object.create(null); + + // Maintain original functionality for type extensions, but substitute our + // more permissive validator for base types to allow value types + return { + InputObjectTypeExtension: checkFieldUniqueness, + InterfaceTypeExtension: checkFieldUniqueness, + ObjectTypeExtension: checkFieldUniqueness, + InputObjectTypeDefinition: checkFieldUniquenessExcludingValueTypes, + InterfaceTypeDefinition: checkFieldUniquenessExcludingValueTypes, + ObjectTypeDefinition: checkFieldUniquenessExcludingValueTypes, + }; + + function checkFieldUniqueness(node: TypeExtensionWithFields) { + const typeName = node.name.value; + + if (!knownFieldNames[typeName]) { + knownFieldNames[typeName] = Object.create(null); + } + + if (!node.fields) { + return false; + } + + const fieldNames = knownFieldNames[typeName]; + + for (const fieldDef of node.fields) { + const fieldName = fieldDef.name.value; + + if (hasField(existingTypeMap[typeName], fieldName)) { + context.reportError( + new GraphQLError( + existedFieldDefinitionNameMessage( + typeName, + fieldName, + existingTypeMap[typeName].astNode!.serviceName!, + ), + fieldDef.name, + ), + ); + } else if (fieldNames[fieldName]) { + context.reportError( + new GraphQLError( + duplicateFieldDefinitionNameMessage(typeName, fieldName), + [fieldNames[fieldName], fieldDef.name], + ), + ); + } else { + fieldNames[fieldName] = fieldDef.name; + } + } + + return false; + } + + /** + * Similar to checkFieldUniqueness above, with some extra permissions: + * + * 1) Non-uniqueness *on value types* (same field names, same field types) should be permitted + * 2) *Near* value types are also permitted here (with relevant errors in uniqueTypeNamesWithFields) + * - Near value types share only the same type name and field names. Permitting these cases allows + * us to catch and warn on likely user errors. + * + * @param node TypeDefinitionWithFields + */ + function checkFieldUniquenessExcludingValueTypes( + node: TypeDefinitionWithFields, + ) { + const typeName = node.name.value; + + const valueTypeFromSchema = + existingTypeMap[typeName] && + (existingTypeMap[typeName].astNode as Maybe); + const duplicateTypeNode = + valueTypeFromSchema || possibleValueTypes[node.name.value]; + + if (duplicateTypeNode) { + const { fields } = diffTypeNodes(node, duplicateTypeNode); + + // This is the condition required for a *near* value type. At this point, we know the + // parent type names are the same. We know the field names are the same if either: + // 1) the field has no entry in the fields diff (they're identical), or + // 2) the field's diff entry is an array of length 2 (both nodes have the field, but the field types are different) + if (Object.values(fields).every(diffEntry => diffEntry.length === 2)) { + return false; + } + } else { + possibleValueTypes[node.name.value] = node; + } + + if (!knownFieldNames[typeName]) { + knownFieldNames[typeName] = Object.create(null); + } + + if (!node.fields) { + return false; + } + + const fieldNames = knownFieldNames[typeName]; + + for (const fieldDef of node.fields) { + const fieldName = fieldDef.name.value; + if (hasField(existingTypeMap[typeName], fieldName)) { + context.reportError( + new GraphQLError( + existedFieldDefinitionNameMessage( + typeName, + fieldName, + existingTypeMap[typeName].astNode!.serviceName!, + ), + fieldDef.name, + ), + ); + } else if (fieldNames[fieldName]) { + context.reportError( + new GraphQLError( + duplicateFieldDefinitionNameMessage(typeName, fieldName), + [fieldNames[fieldName], fieldDef.name], + ), + ); + } else { + fieldNames[fieldName] = fieldDef.name; + } + } + + return false; + } +} + +function hasField(type: GraphQLNamedType, fieldName: string) { + if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) { + return Boolean(type.getFields()[fieldName]); + } + return false; +} diff --git a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts new file mode 100644 index 000000000..5615c6e62 --- /dev/null +++ b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts @@ -0,0 +1,172 @@ +import { + GraphQLError, + ASTVisitor, + TypeDefinitionNode, +} from 'graphql'; + +import { SDLValidationContext } from 'graphql/validation/ValidationContext'; +import { Maybe } from '../../types'; +import { + isTypeNodeAnEntity, + diffTypeNodes, + errorWithCode, + logServiceAndType, +} from '../../utils'; + +export function duplicateTypeNameMessage(typeName: string): string { + return `There can be only one type named "${typeName}".`; +} + +export function existedTypeNameMessage(typeName: string): string { + return `Type "${typeName}" already exists in the schema. It cannot also be defined in this type definition.`; +} + +/** + * Unique type names + * A GraphQL document is only valid if all defined types have unique names. + * Modified to allow duplicate enum and scalar names + */ +export function UniqueTypeNamesWithFields( + context: SDLValidationContext, +): ASTVisitor { + const knownTypes: { + [typeName: string]: TypeDefinitionNode; + } = Object.create(null); + const schema = context.getSchema(); + + return { + ScalarTypeDefinition: checkTypeName, + ObjectTypeDefinition: checkTypeName, + InterfaceTypeDefinition: checkTypeName, + UnionTypeDefinition: checkTypeName, + EnumTypeDefinition: checkTypeName, + InputObjectTypeDefinition: checkTypeName, + }; + + function checkTypeName(node: TypeDefinitionNode) { + const typeName = node.name.value; + const typeFromSchema = schema && schema.getType(typeName); + const typeNodeFromSchema = + typeFromSchema && + (typeFromSchema.astNode as Maybe); + + const typeNodeFromDefs = knownTypes[typeName]; + const duplicateTypeNode = typeNodeFromSchema || typeNodeFromDefs; + + /* + * Return early for value types + * Value types: + * 1) have the same kind (type, interface, input), extensions are excluded + * 2) are not entities + * 3) have the same set of fields + */ + if (duplicateTypeNode) { + const possibleErrors: GraphQLError[] = []; + // By inspecting the diff, we can warn when field types mismatch. + // A diff entry will exist when a field exists on one type and not the other, or if there is a type mismatch on the field + // i.e. { sku: [Int, String!], color: [String] } + const { kind, fields } = diffTypeNodes(node, duplicateTypeNode); + + const fieldsDiff = Object.entries(fields); + + // Error if the kinds don't match + if (kind.length > 0) { + context.reportError( + errorWithCode( + 'VALUE_TYPE_KIND_MISMATCH', + `${logServiceAndType( + duplicateTypeNode.serviceName!, + typeName, + )}Found kind mismatch on expected value type belonging to services \`${ + duplicateTypeNode.serviceName + }\` and \`${ + node.serviceName + }\`. \`${typeName}\` is defined as both a \`${ + kind[0] + }\` and a \`${ + kind[1] + }\`. In order to define \`${typeName}\` in multiple places, the kinds must be identical.`, + [node, duplicateTypeNode], + ), + ); + return; + } + + const typesHaveSameShape = + fieldsDiff.length === 0 || + fieldsDiff.every(([fieldName, types]) => { + // If a diff entry has two types, then the field name matches but the types do not. + // In this case, we can push a useful error to hint to the user that we + // think they tried to define a value type, but one of the fields has a type mismatch. + if (types.length === 2) { + possibleErrors.push( + errorWithCode( + 'VALUE_TYPE_FIELD_TYPE_MISMATCH', + `${logServiceAndType( + duplicateTypeNode.serviceName!, + typeName, + fieldName, + )}A field was defined differently in different services. \`${ + duplicateTypeNode.serviceName + }\` and \`${ + node.serviceName + }\` define \`${typeName}.${fieldName}\` as a ${types[1]} and ${ + types[0] + } respectively. In order to define \`${typeName}\` in multiple places, the fields and their types must be identical.`, + [node, duplicateTypeNode], + ), + ); + return true; + } + return false; + }); + + // Once we determined that types have the same shape (name, kind, and field + // names), we can provide useful errors + if (typesHaveSameShape) { + // Report errors that were collected while determining the matching shape of the types + possibleErrors.forEach(error => context.reportError(error)); + + // Error if either is an entity + if (isTypeNodeAnEntity(node) || isTypeNodeAnEntity(duplicateTypeNode)) { + const entityNode = isTypeNodeAnEntity(duplicateTypeNode) + ? duplicateTypeNode + : node; + + context.reportError( + errorWithCode( + 'VALUE_TYPE_NO_ENTITY', + `${logServiceAndType( + entityNode.serviceName!, + typeName, + )}Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`${typeName}\` type is extended properly or remove the \`@key\` directive if this is not an entity.`, + [node, duplicateTypeNode], + ), + ); + } + + return false; + } + } + + if (typeFromSchema) { + context.reportError( + new GraphQLError(existedTypeNameMessage(typeName), node.name), + ); + return; + } + + if (knownTypes[typeName]) { + context.reportError( + new GraphQLError(duplicateTypeNameMessage(typeName), [ + knownTypes[typeName], + node.name, + ]), + ); + } else { + knownTypes[typeName] = node; + } + + return false; + } +} diff --git a/federation-js/src/csdlDirectives.ts b/federation-js/src/csdlDirectives.ts new file mode 100644 index 000000000..4a3fd103e --- /dev/null +++ b/federation-js/src/csdlDirectives.ts @@ -0,0 +1,95 @@ +import { + GraphQLDirective, + DirectiveLocation, + GraphQLNonNull, + GraphQLString, + GraphQLInt, +} from 'graphql'; + +export const ComposedGraphDirective = new GraphQLDirective({ + name: 'composedGraph', + locations: [DirectiveLocation.SCHEMA], + args: { + version: { + type: GraphQLNonNull(GraphQLInt), + }, + }, +}); + +export const GraphDirective = new GraphQLDirective({ + name: 'graph', + locations: [DirectiveLocation.SCHEMA], + args: { + name: { + type: GraphQLNonNull(GraphQLString), + }, + url: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const OwnerDirective = new GraphQLDirective({ + name: 'owner', + locations: [DirectiveLocation.OBJECT], + args: { + graph: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const KeyDirective = new GraphQLDirective({ + name: 'key', + locations: [DirectiveLocation.OBJECT], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + graph: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const ResolveDirective = new GraphQLDirective({ + name: 'resolve', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + graph: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const ProvidesDirective = new GraphQLDirective({ + name: 'provides', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const RequiresDirective = new GraphQLDirective({ + name: 'requires', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const csdlDirectives = [ + ComposedGraphDirective, + GraphDirective, + OwnerDirective, + KeyDirective, + ResolveDirective, + ProvidesDirective, + RequiresDirective, +]; + +export default csdlDirectives; diff --git a/federation-js/src/directives.ts b/federation-js/src/directives.ts new file mode 100644 index 000000000..dfd723c00 --- /dev/null +++ b/federation-js/src/directives.ts @@ -0,0 +1,126 @@ +import { + GraphQLDirective, + DirectiveLocation, + GraphQLNonNull, + GraphQLString, + GraphQLNamedType, + isInputObjectType, + GraphQLInputObjectType, + DirectiveNode, + ScalarTypeDefinitionNode, + ObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + UnionTypeDefinitionNode, + EnumTypeDefinitionNode, + ScalarTypeExtensionNode, + ObjectTypeExtensionNode, + InterfaceTypeExtensionNode, + UnionTypeExtensionNode, + EnumTypeExtensionNode, + GraphQLField, + FieldDefinitionNode, +} from 'graphql'; + +export const KeyDirective = new GraphQLDirective({ + name: 'key', + locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const ExtendsDirective = new GraphQLDirective({ + name: 'extends', + locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], +}); + +export const ExternalDirective = new GraphQLDirective({ + name: 'external', + locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION], +}); + +export const RequiresDirective = new GraphQLDirective({ + name: 'requires', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const ProvidesDirective = new GraphQLDirective({ + name: 'provides', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + fields: { + type: GraphQLNonNull(GraphQLString), + }, + }, +}); + +export const federationDirectives = [ + KeyDirective, + ExtendsDirective, + ExternalDirective, + RequiresDirective, + ProvidesDirective, +]; + +export default federationDirectives; + +export type ASTNodeWithDirectives = + | ScalarTypeDefinitionNode + | ObjectTypeDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode + | EnumTypeDefinitionNode + | ScalarTypeExtensionNode + | ObjectTypeExtensionNode + | InterfaceTypeExtensionNode + | UnionTypeExtensionNode + | EnumTypeExtensionNode + | FieldDefinitionNode; + +// | GraphQLField +export type GraphQLNamedTypeWithDirectives = Exclude< + GraphQLNamedType, + GraphQLInputObjectType +>; + +function hasDirectives( + node: ASTNodeWithDirectives, +): node is ASTNodeWithDirectives & { + directives: ReadonlyArray; +} { + return Boolean('directives' in node && node.directives); +} + +export function gatherDirectives( + type: GraphQLNamedTypeWithDirectives | GraphQLField, +): DirectiveNode[] { + let directives: DirectiveNode[] = []; + if ('extensionASTNodes' in type && type.extensionASTNodes) { + for (const node of type.extensionASTNodes) { + if (hasDirectives(node)) { + directives = directives.concat(node.directives); + } + } + } + + if (type.astNode && hasDirectives(type.astNode)) + directives = directives.concat(type.astNode.directives); + + return directives; +} + +export function typeIncludesDirective( + type: GraphQLNamedType, + directiveName: string, +): boolean { + if (isInputObjectType(type)) return false; + const directives = gatherDirectives(type as GraphQLNamedTypeWithDirectives); + return directives.some(directive => directive.name.value === directiveName); +} diff --git a/federation-js/src/index.ts b/federation-js/src/index.ts new file mode 100644 index 000000000..036f8a0ad --- /dev/null +++ b/federation-js/src/index.ts @@ -0,0 +1,5 @@ +import "core-js/features/array/flat"; +import "core-js/features/array/flat-map"; + +export * from './composition'; +export * from './service'; diff --git a/federation-js/src/service/__tests__/buildFederatedSchema.test.ts b/federation-js/src/service/__tests__/buildFederatedSchema.test.ts new file mode 100644 index 000000000..3693b1b5a --- /dev/null +++ b/federation-js/src/service/__tests__/buildFederatedSchema.test.ts @@ -0,0 +1,627 @@ +import gql from 'graphql-tag'; +import { Kind, graphql, DocumentNode, execute } from 'graphql'; +import { buildFederatedSchema } from '../buildFederatedSchema'; +import { typeSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(typeSerializer); + +const EMPTY_DOCUMENT = { + kind: Kind.DOCUMENT, + definitions: [], +}; + +describe('buildFederatedSchema', () => { + it(`should mark a type with a key field as an entity`, () => { + const schema = buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` +type Product { + upc: String! + name: String + price: Int +} +`); + + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }); + + it(`should mark a type with multiple key fields as an entity`, () => { + const schema = buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "sku") { + upc: String! + sku: String! + name: String + price: Int + } + `); + + expect(schema.getType('Product')).toMatchInlineSnapshot(` +type Product { + upc: String! + sku: String! + name: String + price: Int +} +`); + + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }); + + it(`should not mark a type without a key field as an entity`, () => { + const schema = buildFederatedSchema(gql` + type Money { + amount: Int! + currencyCode: String! + } + `); + + expect(schema.getType('Money')).toMatchInlineSnapshot(` +type Money { + amount: Int! + currencyCode: String! +} +`); + }); + + it('should preserve description text in generated SDL', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const schema = buildFederatedSchema(gql` + "A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text" + type User @key(fields: "id") { + """ + The unique ID of the user. + """ + id: ID! + "The user's name." + name: String + username: String + foo( + "Description 1" + arg1: String + "Description 2" + arg2: String + "Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3" + arg3: String + ): String + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data?._service.sdl).toEqual(`""" +A user. This user is very complicated and requires so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so so much description text +""" +type User @key(fields: "id") { + """The unique ID of the user.""" + id: ID! + + """The user's name.""" + name: String + username: String + foo( + """Description 1""" + arg1: String + + """Description 2""" + arg2: String + + """ + Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 Description 3 + """ + arg3: String + ): String +} +`); + }); + + describe(`should add an _entities query root field to the schema`, () => { + it(`when a query root type with the default name has been defined`, () => { + const schema = buildFederatedSchema(gql` + type Query { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + rootField: String +} +`); + }); + + it(`when a query root type with a non-default name has been defined`, () => { + const schema = buildFederatedSchema(gql` + schema { + query: QueryRoot + } + + type QueryRoot { + rootField: String + } + type Product @key(fields: "upc") { + upc: ID! + } + `); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` +type QueryRoot { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + rootField: String +} +`); + }); + }); + describe(`should not add an _entities query root field to the schema`, () => { + it(`when no query root type has been defined`, () => { + const schema = buildFederatedSchema(EMPTY_DOCUMENT); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` +type Query { + _service: _Service! +} +`); + }); + it(`when no types with keys are found`, () => { + const schema = buildFederatedSchema(gql` + type Query { + rootField: String + } + `); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` +type Query { + _service: _Service! + rootField: String +} +`); + }); + it(`when only an interface with keys are found`, () => { + const schema = buildFederatedSchema(gql` + type Query { + rootField: String + } + interface Product @key(fields: "upc") { + upc: ID! + } + `); + + expect(schema.getQueryType()).toMatchInlineSnapshot(` +type Query { + _service: _Service! + rootField: String +} +`); + }); + }); + describe('_entities root field', () => { + it('executes resolveReference for a type if found', async () => { + const query = `query GetEntities($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on Product { + name + } + ... on User { + firstName + } + } + }`; + + const variables = { + representations: [ + { __typename: 'Product', upc: 1 }, + { __typename: 'User', id: 1 }, + ], + }; + + const schema = buildFederatedSchema([ + { + typeDefs: gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + type User @key(fields: "id") { + firstName: String + } + `, + resolvers: { + Product: { + __resolveReference(object) { + expect(object.upc).toEqual(1); + return { name: 'Apollo Gateway' }; + }, + }, + User: { + __resolveReference(object) { + expect(object.id).toEqual(1); + return Promise.resolve({ firstName: 'James' }); + }, + }, + }, + }, + ]); + const { data, errors } = await graphql( + schema, + query, + null, + null, + variables, + ); + expect(errors).toBeUndefined(); + expect(data._entities[0].name).toEqual('Apollo Gateway'); + expect(data._entities[1].firstName).toEqual('James'); + }); + it('executes resolveReference with default representation values', async () => { + const query = `query GetEntities($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on Product { + upc + name + } + } + }`; + + const variables = { + representations: [ + { __typename: 'Product', upc: 1, name: 'Apollo Gateway' }, + ], + }; + + const schema = buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: Int + name: String + } + `); + const { data, errors } = await graphql( + schema, + query, + null, + null, + variables, + ); + expect(errors).toBeUndefined(); + expect(data._entities[0].name).toEqual('Apollo Gateway'); + }); + }); + describe('_service root field', () => { + it('keeps extension types when owner type is not present', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const schema = buildFederatedSchema(gql` + type Review { + id: ID + } + + extend type Review { + title: String + } + + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] +} + +type Review { + id: ID + title: String +} +`); + }); + it('keeps extension interface when owner interface is not present', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const schema = buildFederatedSchema(gql` + type Review { + id: ID + } + + extend type Review { + title: String + } + + interface Node @key(fields: "id") { + id: ID! + } + + extend interface Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`interface Node @key(fields: "id") { + id: ID! +} + +extend interface Product @key(fields: "upc") { + upc: String @external + reviews: [Review] +} + +type Review { + id: ID + title: String +} +`); + }); + it('returns valid sdl for @key directives', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const schema = buildFederatedSchema(gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`type Product @key(fields: "upc") { + upc: String! + name: String + price: Int +} +`); + }); + it('returns valid sdl for multiple @key directives', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + const schema = buildFederatedSchema(gql` + type Product @key(fields: "upc") @key(fields: "name") { + upc: String! + name: String + price: Int + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`type Product @key(fields: "upc") @key(fields: "name") { + upc: String! + name: String + price: Int +} +`); + }); + it('supports all federation directives', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + + const schema = buildFederatedSchema(gql` + type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "email") + product: Product @provides(fields: "upc") + } + + extend type User @key(fields: "email") { + email: String @external + reviews: [Review] + } + + extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl) + .toEqual(`extend type Product @key(fields: "upc") { + upc: String @external + reviews: [Review] +} + +type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "email") + product: Product @provides(fields: "upc") +} + +extend type User @key(fields: "email") { + email: String @external + reviews: [Review] +} +`); + }); + it('keeps custom directives', async () => { + const query = `query GetServiceDetails { + _service { + sdl + } + }`; + + const schema = buildFederatedSchema(gql` + directive @custom on FIELD + + extend type User @key(fields: "email") { + email: String @external + } + `); + + const { data, errors } = await graphql(schema, query); + expect(errors).toBeUndefined(); + expect(data._service.sdl).toEqual(`directive @custom on FIELD + +extend type User @key(fields: "email") { + email: String @external +} +`); + }); + }); +}); + +describe('legacy interface', () => { + const resolvers = { + Query: { + product: () => ({}), + }, + Product: { + upc: () => '1234', + price: () => 10, + }, + }; + const typeDefs: DocumentNode[] = [ + gql` + type Query { + product: Product + } + type Product @key(fields: "upc") { + upc: String! + name: String + } + `, + gql` + extend type Product { + price: Int + } + `, + ]; + it('allows legacy schema module interface as an input with an array of typeDefs and resolvers', async () => { + const schema = buildFederatedSchema({ typeDefs, resolvers }); + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + expect( + await execute( + schema, + gql` + { + product { + price + upc + } + } + `, + ), + ).toEqual({ + data: { + product: { upc: '1234', price: 10 }, + }, + }); + }); + it('allows legacy schema module interface as a single module', async () => { + const schema = buildFederatedSchema({ + typeDefs: gql` + type Query { + product: Product + } + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `, + resolvers, + }); + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + expect( + await execute( + schema, + gql` + { + product { + price + upc + } + } + `, + ), + ).toEqual({ + data: { + product: { upc: '1234', price: 10 }, + }, + }); + }); + it('allows legacy schema module interface as a single module without resolvers', async () => { + const schema = buildFederatedSchema({ + typeDefs: gql` + type Query { + product: Product + } + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + } + `, + }); + expect(schema.getType('Product')).toMatchInlineSnapshot(` +type Product { + upc: String! + name: String + price: Int +} +`); + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }); + it('allows legacy schema module interface as a simple array of documents', async () => { + const schema = buildFederatedSchema({ typeDefs }); + expect(schema.getType('Product')).toMatchInlineSnapshot(` +type Product { + upc: String! + name: String + price: Int +} +`); + expect(schema.getType('_Entity')).toMatchInlineSnapshot( + `union _Entity = Product`, + ); + }); +}); diff --git a/federation-js/src/service/__tests__/printComposedSdl.test.ts b/federation-js/src/service/__tests__/printComposedSdl.test.ts new file mode 100644 index 000000000..2d45b62a1 --- /dev/null +++ b/federation-js/src/service/__tests__/printComposedSdl.test.ts @@ -0,0 +1,311 @@ +import { fixtures } from 'apollo-federation-integration-testsuite'; +import { composeAndValidate } from '../../composition'; +import { parse, GraphQLError, visit, StringValueNode } from 'graphql'; + +describe('printComposedSdl', () => { + let composedSdl: string | undefined, errors: GraphQLError[]; + + beforeAll(() => { + // composeAndValidate calls `printComposedSdl` to return `composedSdl` + ({ composedSdl, errors } = composeAndValidate(fixtures)); + }); + + it('composes without errors', () => { + expect(errors).toHaveLength(0); + }); + + it('produces a parseable output', () => { + expect(() => parse(composedSdl!)).not.toThrow(); + }) + + it('prints a fully composed schema correctly', () => { + expect(composedSdl).toMatchInlineSnapshot(` + "schema + @graph(name: \\"accounts\\", url: \\"https://accounts.api.com\\") + @graph(name: \\"books\\", url: \\"https://books.api.com\\") + @graph(name: \\"documents\\", url: \\"https://documents.api.com\\") + @graph(name: \\"inventory\\", url: \\"https://inventory.api.com\\") + @graph(name: \\"product\\", url: \\"https://product.api.com\\") + @graph(name: \\"reviews\\", url: \\"https://reviews.api.com\\") + @composedGraph(version: 1) + { + query: Query + mutation: Mutation + } + + directive @composedGraph(version: Int!) on SCHEMA + + directive @graph(name: String!, url: String!) on SCHEMA + + directive @owner(graph: String!) on OBJECT + + directive @key(fields: String!, graph: String!) on OBJECT + + directive @resolve(graph: String!) on FIELD_DEFINITION + + directive @provides(fields: String!) on FIELD_DEFINITION + + directive @requires(fields: String!) on FIELD_DEFINITION + + directive @stream on FIELD + + directive @transform(from: String!) on FIELD + + union AccountType = PasswordAccount | SMSAccount + + type Amazon { + referrer: String + } + + union Body = Image | Text + + type Book implements Product + @owner(graph: \\"books\\") + @key(fields: \\"{ isbn }\\", graph: \\"books\\") + @key(fields: \\"{ isbn }\\", graph: \\"inventory\\") + @key(fields: \\"{ isbn }\\", graph: \\"product\\") + @key(fields: \\"{ isbn }\\", graph: \\"reviews\\") + { + isbn: String! + title: String + year: Int + similarBooks: [Book]! + metadata: [MetadataOrError] + inStock: Boolean @resolve(graph: \\"inventory\\") + isCheckedOut: Boolean @resolve(graph: \\"inventory\\") + upc: String! @resolve(graph: \\"product\\") + sku: String! @resolve(graph: \\"product\\") + name(delimeter: String = \\" \\"): String @resolve(graph: \\"product\\") @requires(fields: \\"{ title year }\\") + price: String @resolve(graph: \\"product\\") + details: ProductDetailsBook @resolve(graph: \\"product\\") + reviews: [Review] @resolve(graph: \\"reviews\\") + relatedReviews: [Review!]! @resolve(graph: \\"reviews\\") @requires(fields: \\"{ similarBooks { isbn } }\\") + } + + union Brand = Ikea | Amazon + + type Car implements Vehicle + @owner(graph: \\"product\\") + @key(fields: \\"{ id }\\", graph: \\"product\\") + @key(fields: \\"{ id }\\", graph: \\"reviews\\") + { + id: String! + description: String + price: String + retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"{ price }\\") + } + + type Error { + code: Int + message: String + } + + type Furniture implements Product + @owner(graph: \\"product\\") + @key(fields: \\"{ upc }\\", graph: \\"product\\") + @key(fields: \\"{ sku }\\", graph: \\"product\\") + @key(fields: \\"{ sku }\\", graph: \\"inventory\\") + @key(fields: \\"{ upc }\\", graph: \\"reviews\\") + { + upc: String! + sku: String! + name: String + price: String + brand: Brand + metadata: [MetadataOrError] + details: ProductDetailsFurniture + inStock: Boolean @resolve(graph: \\"inventory\\") + isHeavy: Boolean @resolve(graph: \\"inventory\\") + reviews: [Review] @resolve(graph: \\"reviews\\") + } + + type Ikea { + asile: Int + } + + type Image { + name: String! + attributes: ImageAttributes! + } + + type ImageAttributes { + url: String! + } + + type KeyValue { + key: String! + value: String! + } + + type Library + @owner(graph: \\"books\\") + @key(fields: \\"{ id }\\", graph: \\"books\\") + @key(fields: \\"{ id }\\", graph: \\"accounts\\") + { + id: ID! + name: String + userAccount(id: ID! = 1): User @resolve(graph: \\"accounts\\") @requires(fields: \\"{ name }\\") + } + + union MetadataOrError = KeyValue | Error + + type Mutation { + login(username: String!, password: String!): User @resolve(graph: \\"accounts\\") + reviewProduct(upc: String!, body: String!): Product @resolve(graph: \\"reviews\\") + updateReview(review: UpdateReviewInput!): Review @resolve(graph: \\"reviews\\") + deleteReview(id: ID!): Boolean @resolve(graph: \\"reviews\\") + } + + type Name { + first: String + last: String + } + + type PasswordAccount + @owner(graph: \\"accounts\\") + @key(fields: \\"{ email }\\", graph: \\"accounts\\") + { + email: String! + } + + interface Product { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + inStock: Boolean + reviews: [Review] + } + + interface ProductDetails { + country: String + } + + type ProductDetailsBook implements ProductDetails { + country: String + pages: Int + } + + type ProductDetailsFurniture implements ProductDetails { + country: String + color: String + } + + type Query { + user(id: ID!): User @resolve(graph: \\"accounts\\") + me: User @resolve(graph: \\"accounts\\") + book(isbn: String!): Book @resolve(graph: \\"books\\") + books: [Book] @resolve(graph: \\"books\\") + library(id: ID!): Library @resolve(graph: \\"books\\") + body: Body! @resolve(graph: \\"documents\\") + product(upc: String!): Product @resolve(graph: \\"product\\") + vehicle(id: String!): Vehicle @resolve(graph: \\"product\\") + topProducts(first: Int = 5): [Product] @resolve(graph: \\"product\\") + topCars(first: Int = 5): [Car] @resolve(graph: \\"product\\") + topReviews(first: Int = 5): [Review] @resolve(graph: \\"reviews\\") + } + + type Review + @owner(graph: \\"reviews\\") + @key(fields: \\"{ id }\\", graph: \\"reviews\\") + { + id: ID! + body(format: Boolean = false): String + author: User @provides(fields: \\"{ username }\\") + product: Product + metadata: [MetadataOrError] + } + + type SMSAccount + @owner(graph: \\"accounts\\") + @key(fields: \\"{ number }\\", graph: \\"accounts\\") + { + number: String + } + + type Text { + name: String! + attributes: TextAttributes! + } + + type TextAttributes { + bold: Boolean + text: String + } + + union Thing = Car | Ikea + + input UpdateReviewInput { + id: ID! + body: String + } + + type User + @owner(graph: \\"accounts\\") + @key(fields: \\"{ id }\\", graph: \\"accounts\\") + @key(fields: \\"{ username name { first last } }\\", graph: \\"accounts\\") + @key(fields: \\"{ id }\\", graph: \\"inventory\\") + @key(fields: \\"{ id }\\", graph: \\"product\\") + @key(fields: \\"{ id }\\", graph: \\"reviews\\") + { + id: ID! + name: Name + username: String + birthDate(locale: String): String + account: AccountType + metadata: [UserMetadata] + goodDescription: Boolean @resolve(graph: \\"inventory\\") @requires(fields: \\"{ metadata { description } }\\") + vehicle: Vehicle @resolve(graph: \\"product\\") + thing: Thing @resolve(graph: \\"product\\") + reviews: [Review] @resolve(graph: \\"reviews\\") + numberOfReviews: Int! @resolve(graph: \\"reviews\\") + goodAddress: Boolean @resolve(graph: \\"reviews\\") @requires(fields: \\"{ metadata { address } }\\") + } + + type UserMetadata { + name: String + address: String + description: String + } + + type Van implements Vehicle + @owner(graph: \\"product\\") + @key(fields: \\"{ id }\\", graph: \\"product\\") + @key(fields: \\"{ id }\\", graph: \\"reviews\\") + { + id: String! + description: String + price: String + retailPrice: String @resolve(graph: \\"reviews\\") @requires(fields: \\"{ price }\\") + } + + interface Vehicle { + id: String! + description: String + price: String + retailPrice: String + } + " + `); + }); + + it('fieldsets are parseable', () => { + const parsedCsdl = parse(composedSdl!); + const fieldSets: string[] = []; + + // Collect all args with the 'fields' name (from @key, @provides, @requires directives) + visit(parsedCsdl, { + Argument(node) { + if (node.name.value === 'fields') { + fieldSets.push((node.value as StringValueNode).value); + } + }, + }); + + // Ensure each found 'fields' arg is graphql parseable + fieldSets.forEach((unparsed) => { + expect(() => parse(unparsed)).not.toThrow(); + }); + }); +}); diff --git a/federation-js/src/service/__tests__/printFederatedSchema.test.ts b/federation-js/src/service/__tests__/printFederatedSchema.test.ts new file mode 100644 index 000000000..b0eddadb7 --- /dev/null +++ b/federation-js/src/service/__tests__/printFederatedSchema.test.ts @@ -0,0 +1,216 @@ +import { fixtures } from 'apollo-federation-integration-testsuite'; +import { composeAndValidate } from '../../composition'; +import { printSchema } from '../printFederatedSchema'; + +describe('printFederatedSchema', () => { + const { schema, errors } = composeAndValidate(fixtures); + + it('composes without errors', () => { + expect(errors).toHaveLength(0); + }); + + it('prints a fully composed schema correctly', () => { + expect(printSchema(schema)).toMatchInlineSnapshot(` + "directive @stream on FIELD + + directive @transform(from: String!) on FIELD + + union AccountType = PasswordAccount | SMSAccount + + type Amazon { + referrer: String + } + + union Body = Image | Text + + type Book implements Product @key(fields: \\"isbn\\") { + isbn: String! + title: String + year: Int + similarBooks: [Book]! + metadata: [MetadataOrError] + inStock: Boolean + isCheckedOut: Boolean + upc: String! + sku: String! + name(delimeter: String = \\" \\"): String @requires(fields: \\"title year\\") + price: String + details: ProductDetailsBook + reviews: [Review] + relatedReviews: [Review!]! @requires(fields: \\"similarBooks { isbn }\\") + } + + union Brand = Ikea | Amazon + + type Car implements Vehicle @key(fields: \\"id\\") { + id: String! + description: String + price: String + retailPrice: String @requires(fields: \\"price\\") + } + + type Error { + code: Int + message: String + } + + type Furniture implements Product @key(fields: \\"sku\\") @key(fields: \\"upc\\") { + upc: String! + sku: String! + name: String + price: String + brand: Brand + metadata: [MetadataOrError] + details: ProductDetailsFurniture + inStock: Boolean + isHeavy: Boolean + reviews: [Review] + } + + type Ikea { + asile: Int + } + + type Image { + name: String! + attributes: ImageAttributes! + } + + type ImageAttributes { + url: String! + } + + type KeyValue { + key: String! + value: String! + } + + type Library @key(fields: \\"id\\") { + id: ID! + name: String + userAccount(id: ID! = 1): User @requires(fields: \\"name\\") + } + + union MetadataOrError = KeyValue | Error + + type Mutation { + login(username: String!, password: String!): User + reviewProduct(upc: String!, body: String!): Product + updateReview(review: UpdateReviewInput!): Review + deleteReview(id: ID!): Boolean + } + + type Name { + first: String + last: String + } + + type PasswordAccount @key(fields: \\"email\\") { + email: String! + } + + interface Product { + upc: String! + sku: String! + name: String + price: String + details: ProductDetails + inStock: Boolean + reviews: [Review] + } + + interface ProductDetails { + country: String + } + + type ProductDetailsBook implements ProductDetails { + country: String + pages: Int + } + + type ProductDetailsFurniture implements ProductDetails { + country: String + color: String + } + + type Query { + user(id: ID!): User + me: User + book(isbn: String!): Book + books: [Book] + library(id: ID!): Library + body: Body! + product(upc: String!): Product + vehicle(id: String!): Vehicle + topProducts(first: Int = 5): [Product] + topCars(first: Int = 5): [Car] + topReviews(first: Int = 5): [Review] + } + + type Review @key(fields: \\"id\\") { + id: ID! + body(format: Boolean = false): String + author: User @provides(fields: \\"username\\") + product: Product + metadata: [MetadataOrError] + } + + type SMSAccount @key(fields: \\"number\\") { + number: String + } + + type Text { + name: String! + attributes: TextAttributes! + } + + type TextAttributes { + bold: Boolean + text: String + } + + union Thing = Car | Ikea + + input UpdateReviewInput { + id: ID! + body: String + } + + type User @key(fields: \\"id\\") @key(fields: \\"username name { first last }\\") { + id: ID! + name: Name + username: String + birthDate(locale: String): String + account: AccountType + metadata: [UserMetadata] + goodDescription: Boolean @requires(fields: \\"metadata { description }\\") + vehicle: Vehicle + thing: Thing + reviews: [Review] + numberOfReviews: Int! + goodAddress: Boolean @requires(fields: \\"metadata { address }\\") + } + + type UserMetadata { + name: String + address: String + description: String + } + + type Van implements Vehicle @key(fields: \\"id\\") { + id: String! + description: String + price: String + retailPrice: String @requires(fields: \\"price\\") + } + + interface Vehicle { + id: String! + description: String + price: String + retailPrice: String + } + " + `); + }); +}); diff --git a/federation-js/src/service/__tests__/tsconfig.json b/federation-js/src/service/__tests__/tsconfig.json new file mode 100644 index 000000000..4a7651f70 --- /dev/null +++ b/federation-js/src/service/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [{ "path": "../../../" }] +} diff --git a/federation-js/src/service/buildFederatedSchema.ts b/federation-js/src/service/buildFederatedSchema.ts new file mode 100644 index 000000000..16481e090 --- /dev/null +++ b/federation-js/src/service/buildFederatedSchema.ts @@ -0,0 +1,134 @@ +import { + DocumentNode, + GraphQLSchema, + isObjectType, + isUnionType, + GraphQLUnionType, + GraphQLObjectType, + specifiedDirectives, +} from 'graphql'; +import { + buildSchemaFromSDL, + transformSchema, + GraphQLSchemaModule, + modulesFromSDL, + addResolversToSchema, + GraphQLResolverMap, +} from 'apollo-graphql'; +import federationDirectives, { typeIncludesDirective } from '../directives'; + +import { serviceField, entitiesField, EntityType } from '../types'; + +import { printSchema } from './printFederatedSchema'; + +import 'apollo-server-env'; + +type LegacySchemaModule = { + typeDefs: DocumentNode | DocumentNode[]; + resolvers?: GraphQLResolverMap; +}; + +export function buildFederatedSchema( + modulesOrSDL: + | (GraphQLSchemaModule | DocumentNode)[] + | DocumentNode + | LegacySchemaModule, +): GraphQLSchema { + // ApolloServer supports passing an array of DocumentNode along with a single + // map of resolvers to build a schema. Long term we don't want to support this + // style anymore as we move towards a more structured approach to modules, + // however, it has tripped several teams up to not support this signature + // in buildFederatedSchema. Especially as teams migrate from + // `new ApolloServer({ typeDefs: DocumentNode[], resolvers })` to + // `new ApolloServer({ schema: buildFederatedSchema({ typeDefs: DocumentNode[], resolvers }) })` + // + // The last type in the union for `modulesOrSDL` supports this "legacy" input + // style in a simple manner (by just adding the resolvers to the first typeDefs entry) + // + let shapedModulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode; + if ('typeDefs' in modulesOrSDL) { + const { typeDefs, resolvers } = modulesOrSDL; + const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs]; + shapedModulesOrSDL = augmentedTypeDefs.map((typeDefs, i) => { + const module: GraphQLSchemaModule = { typeDefs }; + // add the resolvers to the first "module" in the array + if (i === 0 && resolvers) module.resolvers = resolvers; + return module; + }); + } else { + shapedModulesOrSDL = modulesOrSDL; + } + + const modules = modulesFromSDL(shapedModulesOrSDL); + + let schema = buildSchemaFromSDL( + modules, + new GraphQLSchema({ + query: undefined, + directives: [...specifiedDirectives, ...federationDirectives], + }), + ); + + // At this point in time, we have a schema to be printed into SDL which is + // representative of what the user defined for their schema. This is before + // we process any of the federation directives and add custom federation types + // so its the right place to create our service definition sdl. + // + // We have to use a modified printSchema from graphql-js which includes + // support for preserving the *uses* of federation directives while removing + // their *definitions* from the sdl. + const sdl = printSchema(schema); + + // Add an empty query root type if none has been defined + if (!schema.getQueryType()) { + schema = new GraphQLSchema({ + ...schema.toConfig(), + query: new GraphQLObjectType({ + name: 'Query', + fields: {}, + }), + }); + } + + const entityTypes = Object.values(schema.getTypeMap()).filter( + type => isObjectType(type) && typeIncludesDirective(type, 'key'), + ); + const hasEntities = entityTypes.length > 0; + + schema = transformSchema(schema, type => { + // Add `_entities` and `_service` fields to query root type + if (isObjectType(type) && type === schema.getQueryType()) { + const config = type.toConfig(); + return new GraphQLObjectType({ + ...config, + fields: { + ...(hasEntities && { _entities: entitiesField }), + _service: { + ...serviceField, + resolve: () => ({ sdl }), + }, + ...config.fields, + }, + }); + } + + return undefined; + }); + + schema = transformSchema(schema, type => { + if (hasEntities && isUnionType(type) && type.name === EntityType.name) { + return new GraphQLUnionType({ + ...EntityType.toConfig(), + types: entityTypes.filter(isObjectType), + }); + } + return undefined; + }); + + for (const module of modules) { + if (!module.resolvers) continue; + addResolversToSchema(schema, module.resolvers); + } + + return schema; +} diff --git a/federation-js/src/service/index.ts b/federation-js/src/service/index.ts new file mode 100644 index 000000000..c8cfc974d --- /dev/null +++ b/federation-js/src/service/index.ts @@ -0,0 +1,2 @@ +export * from './buildFederatedSchema'; +export * from './printFederatedSchema'; diff --git a/federation-js/src/service/printComposedSdl.ts b/federation-js/src/service/printComposedSdl.ts new file mode 100644 index 000000000..3ed6f94c8 --- /dev/null +++ b/federation-js/src/service/printComposedSdl.ts @@ -0,0 +1,514 @@ +import { + GraphQLSchema, + isSpecifiedDirective, + isIntrospectionType, + isSpecifiedScalarType, + GraphQLNamedType, + GraphQLDirective, + isScalarType, + isObjectType, + isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, + GraphQLScalarType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLArgument, + GraphQLInputField, + astFromValue, + print, + GraphQLField, + GraphQLEnumValue, + GraphQLString, + DEFAULT_DEPRECATION_REASON, + ASTNode, + SelectionNode, +} from 'graphql'; +import { Maybe, ServiceDefinition, FederationType, FederationField } from '../composition'; +import { isFederationType } from '../types'; +import { isFederationDirective } from '../composition/utils'; +import csdlDirectives from '../csdlDirectives'; + +type Options = { + /** + * Descriptions are defined as preceding string literals, however an older + * experimental version of the SDL supported preceding comments as + * descriptions. Set to true to enable this deprecated behavior. + * This option is provided to ease adoption and will be removed in v16. + * + * Default: false + */ + commentDescriptions?: boolean; +}; + +/** + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * + */ +export function printComposedSdl( + schema: GraphQLSchema, + serviceList: ServiceDefinition[], + options?: Options, +): string { + return printFilteredSchema( + schema, + // Federation change: we need service and url information for the @graph directives + serviceList, + // Federation change: treat the directives defined by the federation spec + // similarly to the directives defined by the GraphQL spec (ie, don't print + // their definitions). + (n) => !isSpecifiedDirective(n) && !isFederationDirective(n), + isDefinedType, + options, + ); +} + +export function printIntrospectionSchema( + schema: GraphQLSchema, + options?: Options, +): string { + return printFilteredSchema( + schema, + [], + isSpecifiedDirective, + isIntrospectionType, + options, + ); +} + +// Federation change: treat the types defined by the federation spec +// similarly to the directives defined by the GraphQL spec (ie, don't print +// their definitions). +function isDefinedType(type: GraphQLNamedType): boolean { + return ( + !isSpecifiedScalarType(type) && + !isIntrospectionType(type) && + !isFederationType(type) + ); +} + +function printFilteredSchema( + schema: GraphQLSchema, + // Federation change: we need service and url information for the @graph directives + serviceList: ServiceDefinition[], + directiveFilter: (type: GraphQLDirective) => boolean, + typeFilter: (type: GraphQLNamedType) => boolean, + options?: Options, +): string { + // Federation change: include directive definitions for CSDL + const directives = [ + ...csdlDirectives, + ...schema.getDirectives().filter(directiveFilter), + ]; + const types = Object.values(schema.getTypeMap()) + .sort((type1, type2) => type1.name.localeCompare(type2.name)) + .filter(typeFilter); + + return ( + [printSchemaDefinition(schema, serviceList)] + .concat( + directives.map(directive => printDirective(directive, options)), + types.map(type => printType(type, options)), + ) + .filter(Boolean) + .join('\n\n') + '\n' + ); +} + +function printSchemaDefinition( + schema: GraphQLSchema, + serviceList: ServiceDefinition[], +): string | undefined { + const operationTypes = []; + + const queryType = schema.getQueryType(); + if (queryType) { + operationTypes.push(` query: ${queryType.name}`); + } + + const mutationType = schema.getMutationType(); + if (mutationType) { + operationTypes.push(` mutation: ${mutationType.name}`); + } + + const subscriptionType = schema.getSubscriptionType(); + if (subscriptionType) { + operationTypes.push(` subscription: ${subscriptionType.name}`); + } + + return ( + 'schema' + + // Federation change: print @graph and @composedGraph schema directives + printFederationSchemaDirectives(serviceList) + + `\n{\n${operationTypes.join('\n')}\n}` + ); +} + +function printFederationSchemaDirectives(serviceList: ServiceDefinition[]) { + return ( + serviceList.map(service => `\n @graph(name: "${service.name}", url: "${service.url}")`).join('') + + `\n @composedGraph(version: 1)` + ); +} + +export function printType(type: GraphQLNamedType, options?: Options): string { + if (isScalarType(type)) { + return printScalar(type, options); + } else if (isObjectType(type)) { + return printObject(type, options); + } else if (isInterfaceType(type)) { + return printInterface(type, options); + } else if (isUnionType(type)) { + return printUnion(type, options); + } else if (isEnumType(type)) { + return printEnum(type, options); + } else if (isInputObjectType(type)) { + return printInputObject(type, options); + } + + throw Error('Unexpected type: ' + (type as GraphQLNamedType).toString()); +} + +function printScalar(type: GraphQLScalarType, options?: Options): string { + return printDescription(options, type) + `scalar ${type.name}`; +} + +function printObject(type: GraphQLObjectType, options?: Options): string { + const interfaces = type.getInterfaces(); + const implementedInterfaces = interfaces.length + ? ' implements ' + interfaces.map(i => i.name).join(' & ') + : ''; + + // Federation change: print `extend` keyword on type extensions. + // + // The implementation assumes that an owned type will have fields defined + // since that is required for a valid schema. Types that are *only* + // extensions will not have fields on the astNode since that ast doesn't + // exist. + // + // XXX revist extension checking + const isExtension = + type.extensionASTNodes && type.astNode && !type.astNode.fields; + + return ( + printDescription(options, type) + + (isExtension ? 'extend ' : '') + + `type ${type.name}` + + implementedInterfaces + + // Federation addition for printing @owner and @key usages + printFederationTypeDirectives(type) + + printFields(options, type) + ); +} + +// Federation change: print usages of the @owner and @key directives. +function printFederationTypeDirectives(type: GraphQLObjectType): string { + const metadata: FederationType = type.extensions?.federation; + if (!metadata) return ''; + + const { serviceName: ownerService, keys } = metadata; + if (!ownerService || !keys) return ''; + + // Separate owner @keys from the rest of the @keys so we can print them + // adjacent to the @owner directive. + const { [ownerService]: ownerKeys, ...restKeys } = keys + const ownerEntry: [string, (readonly SelectionNode[])[]] = [ownerService, ownerKeys]; + const restEntries = Object.entries(restKeys); + + return ( + `\n @owner(graph: "${ownerService}")` + + [ownerEntry, ...restEntries].map(([service, keys]) => + keys + .map( + (selections) => + `\n @key(fields: "${printFieldSet(selections)}", graph: "${service}")`, + ) + .join(''), + ) + .join('') + ); +} + +function printInterface(type: GraphQLInterfaceType, options?: Options): string { + // Federation change: print `extend` keyword on type extensions. + // See printObject for assumptions made. + // + // XXX revist extension checking + const isExtension = + type.extensionASTNodes && type.astNode && !type.astNode.fields; + + return ( + printDescription(options, type) + + (isExtension ? 'extend ' : '') + + `interface ${type.name}` + + printFields(options, type) + ); +} + +function printUnion(type: GraphQLUnionType, options?: Options): string { + const types = type.getTypes(); + const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; + return printDescription(options, type) + 'union ' + type.name + possibleTypes; +} + +function printEnum(type: GraphQLEnumType, options?: Options): string { + const values = type + .getValues() + .map( + (value, i) => + printDescription(options, value, ' ', !i) + + ' ' + + value.name + + printDeprecated(value), + ); + + return ( + printDescription(options, type) + `enum ${type.name}` + printBlock(values) + ); +} + +function printInputObject( + type: GraphQLInputObjectType, + options?: Options, +): string { + const fields = Object.values(type.getFields()).map( + (f, i) => + printDescription(options, f, ' ', !i) + ' ' + printInputValue(f), + ); + return ( + printDescription(options, type) + `input ${type.name}` + printBlock(fields) + ); +} + +function printFields( + options: Options | undefined, + type: GraphQLObjectType | GraphQLInterfaceType, +) { + + const fields = Object.values(type.getFields()).map( + (f, i) => + printDescription(options, f, ' ', !i) + + ' ' + + f.name + + printArgs(options, f.args, ' ') + + ': ' + + String(f.type) + + printDeprecated(f) + + printFederationFieldDirectives(f, type), + ); + + // Federation change: for entities, we want to print the block on a new line. + // This is just a formatting nice-to-have. + const isEntity = Boolean(type.extensions?.federation?.keys); + + return printBlock(fields, isEntity); +} + +export function printWithReducedWhitespace(ast: ASTNode): string { + return print(ast) + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Federation change: print fieldsets for @key, @requires, and @provides directives + * + * @param selections + */ +function printFieldSet(selections: readonly SelectionNode[]): string { + return `{ ${selections.map(printWithReducedWhitespace).join(' ')} }`; +} + +/** + * Federation change: print @resolve, @requires, and @provides directives + * + * @param field + * @param parentType + */ +function printFederationFieldDirectives( + field: GraphQLField, + parentType: GraphQLObjectType | GraphQLInterfaceType, +): string { + if (!field.extensions?.federation) return ''; + + const { + serviceName, + requires = [], + provides = [], + }: FederationField = field.extensions.federation; + + let printed = ''; + // If a `serviceName` exists, we only want to print a `@resolve` directive + // if the `serviceName` differs from the `parentType`'s `serviceName` + if ( + serviceName && + serviceName !== parentType.extensions?.federation?.serviceName + ) { + printed += ` @resolve(graph: "${serviceName}")`; + } + + if (requires.length > 0) { + printed += ` @requires(fields: "${printFieldSet(requires)}")`; + } + + if (provides.length > 0) { + printed += ` @provides(fields: "${printFieldSet(provides)}")`; + } + + return printed; +} + +// Federation change: `onNewLine` is a formatting nice-to-have for printing +// types that have a list of directives attached, i.e. an entity. +function printBlock(items: string[], onNewLine?: boolean) { + return items.length !== 0 + ? onNewLine + ? '\n{\n' + items.join('\n') + '\n}' + : ' {\n' + items.join('\n') + '\n}' + : ''; +} + +function printArgs( + options: Options | undefined, + args: GraphQLArgument[], + indentation = '', +) { + if (args.length === 0) { + return ''; + } + + // If every arg does not have a description, print them on one line. + if (args.every((arg) => !arg.description)) { + return '(' + args.map(printInputValue).join(', ') + ')'; + } + + return ( + '(\n' + + args + .map( + (arg, i) => + printDescription(options, arg, ' ' + indentation, !i) + + ' ' + + indentation + + printInputValue(arg), + ) + .join('\n') + + '\n' + + indentation + + ')' + ); +} + +function printInputValue(arg: GraphQLInputField) { + const defaultAST = astFromValue(arg.defaultValue, arg.type); + let argDecl = arg.name + ': ' + String(arg.type); + if (defaultAST) { + argDecl += ` = ${print(defaultAST)}`; + } + return argDecl; +} + +function printDirective(directive: GraphQLDirective, options?: Options) { + return ( + printDescription(options, directive) + + 'directive @' + + directive.name + + printArgs(options, directive.args) + + (directive.isRepeatable ? ' repeatable' : '') + + ' on ' + + directive.locations.join(' | ') + ); +} + +function printDeprecated( + fieldOrEnumVal: GraphQLField | GraphQLEnumValue, +) { + if (!fieldOrEnumVal.isDeprecated) { + return ''; + } + const reason = fieldOrEnumVal.deprecationReason; + const reasonAST = astFromValue(reason, GraphQLString); + if (reasonAST && reason !== DEFAULT_DEPRECATION_REASON) { + return ' @deprecated(reason: ' + print(reasonAST) + ')'; + } + return ' @deprecated'; +} + +function printDescription }>( + options: Options | undefined, + def: T, + indentation = '', + firstInBlock = true, +): string { + const { description } = def; + if (description == null) { + return ''; + } + + if (options?.commentDescriptions === true) { + return printDescriptionWithComments(description, indentation, firstInBlock); + } + + const preferMultipleLines = description.length > 70; + const blockString = printBlockString(description, '', preferMultipleLines); + const prefix = + indentation && !firstInBlock ? '\n' + indentation : indentation; + + return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; +} + +function printDescriptionWithComments( + description: string, + indentation: string, + firstInBlock: boolean, +) { + const prefix = indentation && !firstInBlock ? '\n' : ''; + const comment = description + .split('\n') + .map((line) => indentation + (line !== '' ? '# ' + line : '#')) + .join('\n'); + + return prefix + comment + '\n'; +} + +/** + * Print a block string in the indented block form by adding a leading and + * trailing blank line. However, if a block string starts with whitespace and is + * a single-line, adding a leading blank line would strip that whitespace. + * + * @internal + */ +export function printBlockString( + value: string, + indentation: string = '', + preferMultipleLines: boolean = false, +): string { + const isSingleLine = value.indexOf('\n') === -1; + const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; + const hasTrailingQuote = value[value.length - 1] === '"'; + const hasTrailingSlash = value[value.length - 1] === '\\'; + const printAsMultipleLines = + !isSingleLine || + hasTrailingQuote || + hasTrailingSlash || + preferMultipleLines; + + let result = ''; + // Format a multi-line block quote to account for leading space. + if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { + result += '\n' + indentation; + } + result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; + if (printAsMultipleLines) { + result += '\n'; + } + + return '"""' + result.replace(/"""/g, '\\"""') + '"""'; +} diff --git a/federation-js/src/service/printFederatedSchema.ts b/federation-js/src/service/printFederatedSchema.ts new file mode 100644 index 000000000..0d963d6c0 --- /dev/null +++ b/federation-js/src/service/printFederatedSchema.ts @@ -0,0 +1,454 @@ +/** + * Forked from graphql-js schemaPrinter.js file @ v14.7.0 + * This file has been modified to support printing federated + * schema, including associated federation directives. + */ + +import { + GraphQLSchema, + isSpecifiedDirective, + isIntrospectionType, + isSpecifiedScalarType, + GraphQLNamedType, + GraphQLDirective, + isScalarType, + isObjectType, + isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, + GraphQLScalarType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLArgument, + GraphQLInputField, + astFromValue, + print, + GraphQLField, + GraphQLEnumValue, + GraphQLString, + DEFAULT_DEPRECATION_REASON, + ASTNode, +} from 'graphql'; +import { Maybe } from '../composition'; +import { isFederationType } from '../types'; +import { isFederationDirective } from '../composition/utils'; +import federationDirectives, { gatherDirectives } from '../directives'; + +type Options = { + /** + * Descriptions are defined as preceding string literals, however an older + * experimental version of the SDL supported preceding comments as + * descriptions. Set to true to enable this deprecated behavior. + * This option is provided to ease adoption and will be removed in v16. + * + * Default: false + */ + commentDescriptions?: boolean; +}; + +/** + * Accepts options as a second argument: + * + * - commentDescriptions: + * Provide true to use preceding comments as the description. + * + */ +export function printSchema(schema: GraphQLSchema, options?: Options): string { + return printFilteredSchema( + schema, + // Federation change: treat the directives defined by the federation spec + // similarly to the directives defined by the GraphQL spec (ie, don't print + // their definitions). + (n) => !isSpecifiedDirective(n) && !isFederationDirective(n), + isDefinedType, + options, + ); +} + +export function printIntrospectionSchema( + schema: GraphQLSchema, + options?: Options, +): string { + return printFilteredSchema( + schema, + isSpecifiedDirective, + isIntrospectionType, + options, + ); +} + +// Federation change: treat the types defined by the federation spec +// similarly to the directives defined by the GraphQL spec (ie, don't print +// their definitions). +function isDefinedType(type: GraphQLNamedType): boolean { + return ( + !isSpecifiedScalarType(type) && + !isIntrospectionType(type) && + !isFederationType(type) + ); +} + +function printFilteredSchema( + schema: GraphQLSchema, + directiveFilter: (type: GraphQLDirective) => boolean, + typeFilter: (type: GraphQLNamedType) => boolean, + options?: Options, +): string { + const directives = schema.getDirectives().filter(directiveFilter); + const types = Object.values(schema.getTypeMap()) + .sort((type1, type2) => type1.name.localeCompare(type2.name)) + .filter(typeFilter); + + return ( + [printSchemaDefinition(schema)] + .concat( + directives.map(directive => printDirective(directive, options)), + types.map(type => printType(type, options)), + ) + .filter(Boolean) + .join('\n\n') + '\n' + ); +} + +function printSchemaDefinition(schema: GraphQLSchema): string | undefined { + if (isSchemaOfCommonNames(schema)) { + return; + } + + const operationTypes = []; + + const queryType = schema.getQueryType(); + if (queryType) { + operationTypes.push(` query: ${queryType.name}`); + } + + const mutationType = schema.getMutationType(); + if (mutationType) { + operationTypes.push(` mutation: ${mutationType.name}`); + } + + const subscriptionType = schema.getSubscriptionType(); + if (subscriptionType) { + operationTypes.push(` subscription: ${subscriptionType.name}`); + } + + return `schema {\n${operationTypes.join('\n')}\n}`; +} + +/** + * GraphQL schema define root types for each type of operation. These types are + * the same as any other type and can be named in any manner, however there is + * a common naming convention: + * + * schema { + * query: Query + * mutation: Mutation + * } + * + * When using this naming convention, the schema description can be omitted. + */ +function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { + const queryType = schema.getQueryType(); + if (queryType && queryType.name !== 'Query') { + return false; + } + + const mutationType = schema.getMutationType(); + if (mutationType && mutationType.name !== 'Mutation') { + return false; + } + + const subscriptionType = schema.getSubscriptionType(); + if (subscriptionType && subscriptionType.name !== 'Subscription') { + return false; + } + + return true; +} + +export function printType(type: GraphQLNamedType, options?: Options): string { + if (isScalarType(type)) { + return printScalar(type, options); + } else if (isObjectType(type)) { + return printObject(type, options); + } else if (isInterfaceType(type)) { + return printInterface(type, options); + } else if (isUnionType(type)) { + return printUnion(type, options); + } else if (isEnumType(type)) { + return printEnum(type, options); + } else if (isInputObjectType(type)) { + return printInputObject(type, options); + } + + throw Error('Unexpected type: ' + (type as GraphQLNamedType).toString()); +} + +function printScalar(type: GraphQLScalarType, options?: Options): string { + return printDescription(options, type) + `scalar ${type.name}`; +} + +function printObject(type: GraphQLObjectType, options?: Options): string { + const interfaces = type.getInterfaces(); + const implementedInterfaces = interfaces.length + ? ' implements ' + interfaces.map(i => i.name).join(' & ') + : ''; + + // Federation change: print `extend` keyword on type extensions. + // + // The implementation assumes that an owned type will have fields defined + // since that is required for a valid schema. Types that are *only* + // extensions will not have fields on the astNode since that ast doesn't + // exist. + // + // XXX revist extension checking + const isExtension = + type.extensionASTNodes && type.astNode && !type.astNode.fields; + + return ( + printDescription(options, type) + + (isExtension ? 'extend ' : '') + + `type ${type.name}${implementedInterfaces}` + + // Federation addition for printing @key usages + printFederationDirectives(type) + + printFields(options, type) + ); +} + +function printInterface(type: GraphQLInterfaceType, options?: Options): string { + // Federation change: print `extend` keyword on type extensions. + // See printObject for assumptions made. + // + // XXX revist extension checking + const isExtension = + type.extensionASTNodes && type.astNode && !type.astNode.fields; + + return ( + printDescription(options, type) + + (isExtension ? 'extend ' : '') + + `interface ${type.name}` + + // Federation change: graphql@14 doesn't support interfaces implementing interfaces + // printImplementedInterfaces(type) + + printFederationDirectives(type) + + printFields(options, type) + ); +} + +function printUnion(type: GraphQLUnionType, options?: Options): string { + const types = type.getTypes(); + const possibleTypes = types.length ? ' = ' + types.join(' | ') : ''; + return printDescription(options, type) + 'union ' + type.name + possibleTypes; +} + +function printEnum(type: GraphQLEnumType, options?: Options): string { + const values = type + .getValues() + .map( + (value, i) => + printDescription(options, value, ' ', !i) + + ' ' + + value.name + + printDeprecated(value), + ); + + return ( + printDescription(options, type) + `enum ${type.name}` + printBlock(values) + ); +} + +function printInputObject(type: GraphQLInputObjectType, options?: Options): string { + const fields = Object.values(type.getFields()).map( + (f, i) => + printDescription(options, f, ' ', !i) + ' ' + printInputValue(f), + ); + return ( + printDescription(options, type) + `input ${type.name}` + printBlock(fields) + ); +} + +function printFields( + options: Options | undefined, + type: GraphQLObjectType | GraphQLInterfaceType, +) { + const fields = Object.values(type.getFields()).map( + (f, i) => + printDescription(options, f, ' ', !i) + + ' ' + + f.name + + printArgs(options, f.args, ' ') + + ': ' + + String(f.type) + + printDeprecated(f) + + printFederationDirectives(f), + ); + return printBlock(fields); +} + +// Federation change: *do* print the usages of federation directives. +function printFederationDirectives( + type: GraphQLNamedType | GraphQLField, +): string { + if (!type.astNode) return ''; + if (isInputObjectType(type)) return ''; + + const allDirectives = gatherDirectives(type) + .filter((n) => + federationDirectives.some((fedDir) => fedDir.name === n.name.value), + ) + .map(print); + const dedupedDirectives = [...new Set(allDirectives)]; + + return dedupedDirectives.length > 0 ? ' ' + dedupedDirectives.join(' ') : ''; +} + +export function printWithReducedWhitespace(ast: ASTNode): string { + return print(ast) + .replace(/\s+/g, ' ') + .trim(); +} + +function printBlock(items: string[]) { + return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : ''; +} + +function printArgs( + options: Options | undefined, + args: GraphQLArgument[], + indentation = '', +) { + if (args.length === 0) { + return ''; + } + + // If every arg does not have a description, print them on one line. + if (args.every(arg => !arg.description)) { + return '(' + args.map(printInputValue).join(', ') + ')'; + } + + return ( + '(\n' + + args + .map( + (arg, i) => + printDescription(options, arg, ' ' + indentation, !i) + + ' ' + + indentation + + printInputValue(arg), + ) + .join('\n') + + '\n' + + indentation + + ')' + ); +} + +function printInputValue(arg: GraphQLInputField) { + const defaultAST = astFromValue(arg.defaultValue, arg.type); + let argDecl = arg.name + ': ' + String(arg.type); + if (defaultAST) { + argDecl += ` = ${print(defaultAST)}`; + } + return argDecl; +} + +function printDirective(directive: GraphQLDirective, options?: Options) { + return ( + printDescription(options, directive) + + 'directive @' + + directive.name + + printArgs(options, directive.args) + + (directive.isRepeatable ? ' repeatable' : '') + + ' on ' + + directive.locations.join(' | ') + ); +} + +function printDeprecated( + fieldOrEnumVal: GraphQLField | GraphQLEnumValue, +) { + if (!fieldOrEnumVal.isDeprecated) { + return ''; + } + const reason = fieldOrEnumVal.deprecationReason; + const reasonAST = astFromValue(reason, GraphQLString); + if (reasonAST && reason !== '' && reason !== DEFAULT_DEPRECATION_REASON) { + return ' @deprecated(reason: ' + print(reasonAST) + ')'; + } + return ' @deprecated'; +} + +function printDescription }>( + options: Options | undefined, + def: T, + indentation = '', + firstInBlock = true, +): string { + const { description } = def; + if (description == null) { + return ''; + } + + if (options?.commentDescriptions === true) { + return printDescriptionWithComments(description, indentation, firstInBlock); + } + + const preferMultipleLines = description.length > 70; + const blockString = printBlockString(description, '', preferMultipleLines); + const prefix = + indentation && !firstInBlock ? '\n' + indentation : indentation; + + return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; +} + +function printDescriptionWithComments( + description: string, + indentation: string, + firstInBlock: boolean, +) { + const prefix = indentation && !firstInBlock ? '\n' : ''; + const comment = description + .split('\n') + .map(line => indentation + (line !== '' ? '# ' + line : '#')) + .join('\n'); + + return prefix + comment + '\n'; +} + +/** + * Print a block string in the indented block form by adding a leading and + * trailing blank line. However, if a block string starts with whitespace and is + * a single-line, adding a leading blank line would strip that whitespace. + * + * @internal + */ +export function printBlockString( + value: string, + indentation: string = '', + preferMultipleLines: boolean = false, +): string { + const isSingleLine = value.indexOf('\n') === -1; + const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; + const hasTrailingQuote = value[value.length - 1] === '"'; + const hasTrailingSlash = value[value.length - 1] === '\\'; + const printAsMultipleLines = + !isSingleLine || + hasTrailingQuote || + hasTrailingSlash || + preferMultipleLines; + + let result = ''; + // Format a multi-line block quote to account for leading space. + if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { + result += '\n' + indentation; + } + result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; + if (printAsMultipleLines) { + result += '\n'; + } + + return '"""' + result.replace(/"""/g, '\\"""') + '"""'; +} diff --git a/federation-js/src/snapshotSerializers/astSerializer.ts b/federation-js/src/snapshotSerializers/astSerializer.ts new file mode 100644 index 000000000..b3dc7eb54 --- /dev/null +++ b/federation-js/src/snapshotSerializers/astSerializer.ts @@ -0,0 +1,21 @@ +import { ASTNode, print } from 'graphql'; +import { Plugin, Config, Refs } from 'pretty-format'; + +export default { + test(value: any) { + return value && typeof value.kind === 'string'; + }, + + serialize( + value: ASTNode, + _config: Config, + indentation: string, + _depth: number, + _refs: Refs, + _printer: any, + ): string { + return print(value) + .trim() + .replace(/\n/g, '\n' + indentation); + }, +} as Plugin; diff --git a/federation-js/src/snapshotSerializers/graphqlErrorSerializer.ts b/federation-js/src/snapshotSerializers/graphqlErrorSerializer.ts new file mode 100644 index 000000000..3363d747d --- /dev/null +++ b/federation-js/src/snapshotSerializers/graphqlErrorSerializer.ts @@ -0,0 +1,15 @@ +import { GraphQLError } from 'graphql'; +import { Plugin } from 'pretty-format'; + +export default { + test(value: any) { + return value && value instanceof GraphQLError; + }, + + print(value: GraphQLError, print) { + return print({ + message: value.message, + code: value.extensions ? value.extensions.code : 'MISSING_ERROR', + }); + }, +} as Plugin; diff --git a/federation-js/src/snapshotSerializers/index.ts b/federation-js/src/snapshotSerializers/index.ts new file mode 100644 index 000000000..df46968ba --- /dev/null +++ b/federation-js/src/snapshotSerializers/index.ts @@ -0,0 +1,15 @@ +export { default as astSerializer } from './astSerializer'; +export { default as selectionSetSerializer } from './selectionSetSerializer'; +export { default as typeSerializer } from './typeSerializer'; +export { default as graphqlErrorSerializer } from './graphqlErrorSerializer'; + +declare global { + namespace jest { + interface Expect { + /** + * Adds a module to format application-specific data structures for serialization. + */ + addSnapshotSerializer(serializer: import('pretty-format').Plugin): void; + } + } +} diff --git a/federation-js/src/snapshotSerializers/selectionSetSerializer.ts b/federation-js/src/snapshotSerializers/selectionSetSerializer.ts new file mode 100644 index 000000000..33dfba417 --- /dev/null +++ b/federation-js/src/snapshotSerializers/selectionSetSerializer.ts @@ -0,0 +1,13 @@ +import { print, SelectionNode, isSelectionNode } from 'graphql'; +import { Plugin } from 'pretty-format'; + +export default { + test(value: any) { + return ( + Array.isArray(value) && value.length > 0 && value.every(isSelectionNode) + ); + }, + print(selectionNodes: SelectionNode[]): string { + return selectionNodes.map(node => print(node)).join('\n'); + }, +} as Plugin; diff --git a/federation-js/src/snapshotSerializers/typeSerializer.ts b/federation-js/src/snapshotSerializers/typeSerializer.ts new file mode 100644 index 000000000..7b78e18ca --- /dev/null +++ b/federation-js/src/snapshotSerializers/typeSerializer.ts @@ -0,0 +1,11 @@ +import { isNamedType, GraphQLNamedType, printType } from 'graphql'; +import { Plugin } from 'pretty-format'; + +export default { + test(value: any) { + return value && isNamedType(value); + }, + print(value: GraphQLNamedType) { + return printType(value); + }, +} as Plugin; diff --git a/federation-js/src/types.ts b/federation-js/src/types.ts new file mode 100644 index 000000000..453835b59 --- /dev/null +++ b/federation-js/src/types.ts @@ -0,0 +1,124 @@ +import { + GraphQLFieldConfig, + GraphQLString, + GraphQLUnionType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLNonNull, + GraphQLList, + GraphQLType, + GraphQLNamedType, + isNamedType, + GraphQLResolveInfo, + isObjectType, +} from 'graphql'; +import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue'; + +export const EntityType = new GraphQLUnionType({ + name: '_Entity', + types: [], +}); + +export const ServiceType = new GraphQLObjectType({ + name: '_Service', + fields: { + sdl: { + type: GraphQLString, + description: + 'The sdl representing the federated service capabilities. Includes federation directives, removes federation types, and includes rest of full schema after schema directives have been applied', + }, + }, +}); + +export const AnyType = new GraphQLScalarType({ + name: '_Any', + serialize(value) { + return value; + }, +}); + +function isPromise(value: PromiseOrValue): value is Promise { + return Boolean(value && 'then' in value && typeof value.then === 'function'); +} + +function addTypeNameToPossibleReturn( + maybeObject: null | T, + typename: string, +): null | T & { __typename: string } { + if (maybeObject !== null && typeof maybeObject === 'object') { + Object.defineProperty(maybeObject, '__typename', { + value: typename, + }); + } + return maybeObject as null | T & { __typename: string }; +} + +export type GraphQLReferenceResolver = ( + reference: object, + context: TContext, + info: GraphQLResolveInfo, +) => any; + +declare module 'graphql/type/definition' { + interface GraphQLObjectType { + resolveReference?: GraphQLReferenceResolver; + } + + interface GraphQLObjectTypeConfig { + resolveReference?: GraphQLReferenceResolver; + } +} + +export const entitiesField: GraphQLFieldConfig = { + type: new GraphQLNonNull(new GraphQLList(EntityType)), + args: { + representations: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AnyType))), + }, + }, + resolve(_source, { representations }, context, info) { + return representations.map((reference: { __typename: string } & object) => { + const { __typename } = reference; + + const type = info.schema.getType(__typename); + if (!type || !isObjectType(type)) { + throw new Error( + `The _entities resolver tried to load an entity for type "${__typename}", but no object type of that name was found in the schema`, + ); + } + + const resolveReference = type.resolveReference + ? type.resolveReference + : function defaultResolveReference() { + return reference; + }; + + // FIXME somehow get this to show up special in Engine traces? + const result = resolveReference(reference, context, info); + + if (isPromise(result)) { + return result.then((x: any) => + addTypeNameToPossibleReturn(x, __typename), + ); + } + + return addTypeNameToPossibleReturn(result, __typename); + }); + }, +}; + +export const serviceField: GraphQLFieldConfig = { + type: new GraphQLNonNull(ServiceType), +}; + +export const federationTypes: GraphQLNamedType[] = [ + ServiceType, + AnyType, + EntityType, +]; + +export function isFederationType(type: GraphQLType): boolean { + return ( + isNamedType(type) && federationTypes.some(({ name }) => name === type.name) + ); +} diff --git a/federation-js/tsconfig.json b/federation-js/tsconfig.json new file mode 100644 index 000000000..76863124d --- /dev/null +++ b/federation-js/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "lib": ["es2017", "es2019.array", "esnext.asynciterable"], + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__"], + "references": [ + { "path": "../federation-integration-testsuite-js" } + ] +} diff --git a/gateway-js/.gitignore b/gateway-js/.gitignore new file mode 100644 index 000000000..93d3f513b --- /dev/null +++ b/gateway-js/.gitignore @@ -0,0 +1,2 @@ +# JS: Ignore the compiled output. +dist/ diff --git a/gateway-js/.npmignore b/gateway-js/.npmignore new file mode 100644 index 000000000..a165046d3 --- /dev/null +++ b/gateway-js/.npmignore @@ -0,0 +1,6 @@ +* +!src/**/* +!dist/**/* +dist/**/*.test.* +!package.json +!README.md diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md new file mode 100644 index 000000000..49138fdb4 --- /dev/null +++ b/gateway-js/CHANGELOG.md @@ -0,0 +1,234 @@ +# CHANGELOG for `@apollo/gateway` + +## vNEXT + +> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. + +- _Nothing yet! Stay tuned!_ + +## v0.20.0 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.19.1 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.19.0 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.18.1 + +- __FIX__: Pass null required fields correctly within the parent object to resolvers. When a composite field was null, it would sometimes be expanded into an object with all null subfields and passed to the resolver. This fix prevents this expansion and sets the field to null, as originally intended. [PR #4157](https://github.com/apollographql/apollo-server/pull/4157) +- __FIX__: Prevent gateway from entering an inoperable state after an initial configuration load failure. [PR #4277](https://github.com/apollographql/apollo-server/pull/4277) + +## v0.18.0 + +- The `RemoteGraphQLDataSource`'s `didEncounterError` method will now receive [`Response`](https://github.com/apollographql/apollo-server/blob/43470d6561bee31101f3afc56bdd154db3f92b30/packages/apollo-server-env/src/fetch.d.ts#L98-L111) as the third argument when it is available, making its signature `(error: Error, fetchRequest: Request, fetchResponse?: Response)`. This compliments the existing [`Request`](https://github.com/apollographql/apollo-server/blob/43470d6561bee31101f3afc56bdd154db3f92b30/packages/apollo-server-env/src/fetch.d.ts#L37-L45) type it was already receiving. Both of these types are [HTTP WHATWG Fetch API](https://fetch.spec.whatwg.org/) types, not `GraphQLRequest`, `GraphQLResponse` types. + +## v0.17.0 + +- __BREAKING__: Move federation metadata from custom objects on schema nodes over to the `extensions` field on schema nodes which are intended for metadata. This is a breaking change because it narrows the `graphql` peer dependency from `^14.0.2` to `^14.5.0` which is when [`extensions` were introduced](https://github.com/graphql/graphql-js/pull/2097) for all Type System objects. [PR #4313](https://github.com/apollographql/apollo-server/pull/4313) + +## v0.16.11 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.16.10 + +- The default branch of the repository has been changed to `main`. As this changed a number of references in the repository's `package.json` and `README.md` files (e.g., for badges, links, etc.), this necessitates a release to publish those changes to npm. [PR #4302](https://github.com/apollographql/apollo-server/pull/4302) +- __FIX__: The cache implementation for the HTTP-fetcher which is used when communicating with the Apollo Registry when the gateway is configured to use [managed federation](https://www.apollographql.com/docs/graph-manager/managed-federation/overview/) will no longer write to its cache when it receives a 304 response. This is necessary since such a response indicates that the cache used to conditionally make the request must already be present. This does not affect GraphQL requests at runtime, only the polling and fetching mechanism for retrieving composed schemas under manged federation. [PR #4325](https://github.com/apollographql/apollo-server/pull/4325) +- __FIX__: The `mergeFieldNodeSelectionSets` method no longer mutates original FieldNode objects. Before, it was updating the selection set of the original object, corrupting the data accross requests. + +## v0.16.9 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.16.7 + +- Bumped the version of `apollo-server-core`, but no other changes! + +## v0.16.6 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.16.5 + +- Only changes in the similarly versioned `@apollo/federation` package. + +## v0.16.4 + +- __NEW__: Provide the `requestContext` as an argument to the experimental callback function `experimental_didResolveQueryPlan`. [#4173](https://github.com/apollographql/apollo-server/pull/4173) + +## v0.16.3 + +- This updates a dependency of `apollo-server-core` that is only used for its TypeScript typings, not for any runtime dependencies. The reason for the upgrade is that the `apollo-server-core` package (again, used only for types!) was affected by a GitHub Security Advisory. [See the related `CHANGELOG.md` for Apollo Server for more details, including a link to the advisory](https://github.com/apollographql/apollo-server/blob/354d9910e1c87af93c7d50263a28554b449e48db/CHANGELOG.md#v2142). + +## v0.16.2 + +- __FIX__: Collapse nested required fields into a single body in the query plan. Before, some nested fields' selection sets were getting split, causing some of their subfields to be dropped when executing the query. This fix collapses the split selection sets into one. [#4064](https://github.com/apollographql/apollo-server/pull/4064) + +## v0.16.1 + +- __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149) + + The `fetcher` specified should be a compliant implementor of the [Fetch API standard](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). This addition compliments, though is still orthognonal to, similar behavior originally introduced in [#3783](https://github.com/apollographql/apollo-server/pull/3783), which allowed customization of the implementation used to fetch _gateway configuration and federated SDL from services_ in managed and unmanaged modes, but didn't affect the communication that takes place during _operation execution_. + + For now, the default `fetcher` will remain the same ([`node-fetch`](https://npm.im/node-fetch)) implementation. A future major-version bump will update it to be consistent with other feature-rich implementations of the Fetch API which are used elsewhere in the Apollo Server stack where we use [`make-fetch-happen`](https://npm.im/make-fetch-happen). In all likelihood, `ApolloGateway` will pass its own `fetcher` to the `RemoteGraphQLDataSource` during service initialization. + +## v0.16.0 + +- __BREAKING__: Use a content delivery network for managed configuration, fetch storage secrets and composition configuration from different domains: https://storage-secrets.api.apollographql.com and https://federation.api.apollographql.com. Please mind any firewall for outgoing traffic. [#4080](https://github.com/apollographql/apollo-server/pull/4080) + +## v0.15.1 + +- __FIX__: Correctly handle unions with nested conditions that have no `possibleTypes` [#4071](https://github.com/apollographql/apollo-server/pull/4071) +- __FIX__: Normalize root operation types when reporting to Apollo Graph Manager. Federation always uses the default names `Query`, `Mutation`, and `Subscription` for root operation types even if downstream services choose different names; now we properly normalize traces received from downstream services in the same way. [#4100](https://github.com/apollographql/apollo-server/pull/4100) + +## v0.15.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e37384a49b2bf474eed0de3e9f4a1bebaeee64c7) + +- __BREAKING__: Drop support for Node.js 8 and Node.js 10. This is being done primarily for performance gains which stand to be seen by transpiling to a newer ECMAScript target. For more details, see the related PR. [#4031](https://github.com/apollographql/apollo-server/pull/4031) +- __Performance:__ Cache stringified representations of downstream query bodies within the query plan to address performance cost incurred by repeatedly `print`ing the same`DocumentNode`s with the `graphql` printer. This improvement is more pronounced on larger documents. [PR #4018](https://github.com/apollographql/apollo-server/pull/4018) +- __Deprecation:__ Deprecated the `ENGINE_API_KEY` environment variable in favor of its new name, `APOLLO_KEY`. The new name mirrors the name used within Apollo Graph Manager. Aside from the rename, the functionality remains otherwise identical. Continued use of `ENGINE_API_KEY` will result in deprecation warnings being printed to the server console. Support for `ENGINE_API_KEY` will be removed in a future, major update. [#3923](https://github.com/apollographql/apollo-server/pull/3923) +- __Deprecation:__ Deprecated the `APOLLO_SCHEMA_TAG` environment variable in favor of its new name, `APOLLO_GRAPH_VARIANT`. The new name mirrors the name used within Apollo Graph Manager. Aside from the rename, the functionality remains otherwise identical. Use of the now-deprecated name will result in a deprecation warning being printed to the server console. Support will be removed entirely in a future, major update. To avoid misconfiguration, runtime errors will be thrown if the new and deprecated versions are _both_ set. [#3855](https://github.com/apollographql/apollo-server/pull/3855) +- Add inadvertently excluded `apollo-server-errors` runtime dependency. [#3927](https://github.com/apollographql/apollo-server/pull/3927) + +## v0.14.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b898396e9fcd3b9092b168f9aac8466ca186fa6b) + +- __FIX__: Resolve condition which surfaced in `0.14.0` which prevented loading the configuration using managed federation. [PR #3979](https://github.com/apollographql/apollo-server/pull/3979) + +## v0.14.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/71a3863f59f4ab2c9052c316479d94c6708c4309) + +- Several previously unhandled Promise rejection errors stemming from, e.g. connectivity, failures when communicating with Apollo Graph Manager within asynchronous code are now handled. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) +- Provide a more helpful error message when encountering expected errors. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) +- General improvements and clarity to error messages and logging. [PR #3811](https://github.com/apollographql/apollo-server/pull/3811) +- Warn of a possible misconfiguration when local service configuration is provided (via `serviceList` or `localServiceList`) and a remote Apollo Graph Manager configuration is subsequently found as well. [PR #3868](https://github.com/apollographql/apollo-server/pull/3868) +- During composition, the unavailability of a downstream service in unmanaged federation mode will no longer result in a partially composed schema which merely lacks the types provided by the downed service. This prevents unexpected validation errors for clients querying a graph which lacks types which were merely unavailable during the initial composition but were intended to be part of the graph. [PR #3867](https://github.com/apollographql/apollo-server/pull/3867) +- Support providing a custom logger implementation (e.g. [`winston`](https://npm.im/winston), [`bunyan`](https://npm.im/bunyan), etc.) to capture gateway-sourced console output. This allows the use of existing, production logging facilities or the possibiltiy to use advanced structure in logging, such as console output which is encapsulated in JSON. The same PR that introduces this support also introduces a `logger` property to the `GraphQLRequestContext` that is exposed to `GraphQLDataSource`s and Apollo Server plugins, making it possible to attach additional properties (as supported by the logger implementation) to specific requests, if desired, by leveraging custom implementations in those components respectively. When not provided, these will still output to `console`. [PR #3894](https://github.com/apollographql/apollo-server/pull/3894) +- Drop use of `loglevel-debug`. This removes the very long date and time prefix in front of each log line and also the support for the `DEBUG=apollo-gateway:` environment variable. Both of these were uncommonly necessary or seldom used (with the environment variable also being undocumented). The existing behavior can be preserved by providing a `logger` that uses `loglevel-debug`, if desired, and more details can be found in the PR. [PR #3896](https://github.com/apollographql/apollo-server/pull/3896) +- Fix Typescript generic typing for datasource contexts [#3865](https://github.com/apollographql/apollo-server/pull/3865) This is a fix for the `TContext` typings of the gateway's exposed `GraphQLDataSource` implementations. In their current form, they don't work as intended, or in any manner that's useful for typing the `context` property throughout the class methods. This introduces a type argument `TContext` to the class itself (which defaults to `Record` for existing implementations) and removes the non-operational type arguments on the class methods themselves. +- Implement retry logic for requests to GCS [PR #3836](https://github.com/apollographql/apollo-server/pull/3836) Note: coupled with this change is a small alteration in how the gateway polls GCS for updates in managed mode. Previously, the tick was on a specific interval. Now, every tick starts after the round of fetches to GCS completes. For more details, see the linked PR. +- Gateway issues health checks to downstream services via `serviceHealthCheck` configuration option. Note: expected behavior differs between managed and unmanaged federation. See PR for new test cases and documentation. [#3930](https://github.com/apollographql/apollo-server/pull/3930) + + +## v0.13.2 + +- __BREAKING__: The behavior and signature of `RemoteGraphQLDataSource`'s `didReceiveResponse` method has been changed. No changes are necessary _unless_ your implementation has overridden the default behavior of this method by either extending the class and overriding the method or by providing `didReceiveResponse` as a parameter to the `RemoteGraphQLDataSource`'s constructor options. Implementations which have provided their own `didReceiveResponse` using either of these methods should view the PR linked here for details on what has changed. [PR #3743](https://github.com/apollographql/apollo-server/pull/3743) +- __NEW__: Setting the `apq` option to `true` on the `RemoteGraphQLDataSource` will enable the use of [automated persisted queries (APQ)](https://www.apollographql.com/docs/apollo-server/performance/apq/) when sending queries to downstream services. Depending on the complexity of queries sent to downstream services, this technique can greatly reduce the size of the payloads being transmitted over the network. Downstream implementing services must also support APQ functionality to participate in this feature (Apollo Server does by default unless it has been explicitly disabled). As with normal APQ behavior, a downstream server must have received and registered a query once before it will be able to serve an APQ request. [#3744](https://github.com/apollographql/apollo-server/pull/3744) +- __NEW__: Experimental feature: compress downstream requests via generated fragments [#3791](https://github.com/apollographql/apollo-server/pull/3791) This feature enables the gateway to generate fragments for queries to downstream services in order to minimize bytes over the wire and parse time. This can be enabled via the gateway config by setting `experimental_autoFragmentization: true`. It is currently disabled by default. +- Introduce `make-fetch-happen` package. Remove `cachedFetcher` in favor of the caching implementation provided by this package. [#3783](https://github.com/apollographql/apollo-server/pull/3783/files) + +## v0.12.1 + +- Update to include [fixes from `@apollo/federation`](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-federation/CHANGELOG.md). + +## v0.12.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/9c0aa1e661ccc2c5a1471b781102637dd47e21b1) + +- Reduce interface expansion for types contained to a single service [#3582](https://github.com/apollographql/apollo-server/pull/3582) +- Instantiate one `CachedFetcher` per gateway instance. This resolves a condition where multiple federated gateways would utilize the same cache store could result in an `Expected undefined to be a GraphQLSchema` error. [#3704](https://github.com/apollographql/apollo-server/pull/3704) +- Gateway: minimize downstream request size [#3737](https://github.com/apollographql/apollo-server/pull/3737) +- experimental: Allow configuration of the query plan store by introducing an `experimental_approximateQueryPlanStoreMiB` property to the `ApolloGateway` constructor options which overrides the default cache size of 30MiB. [#3755](https://github.com/apollographql/apollo-server/pull/3755) + +## v0.11.6 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/0743d6b2f1737758cf09e80d2086917772bc00c9) + +- Fix onSchemaChange callbacks for unmanaged configs [#3605](https://github.com/apollographql/apollo-server/pull/3605) + +## v0.11.4 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a0a60e73e04e913d388de8324f7d17e4406deea2) + + * Gateway over-merging fields of unioned types [#3581](https://github.com/apollographql/apollo-server/pull/3581) + +## v0.11.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/93002737d53dd9a50b473ab9cef14849b3e539aa) + +- Begin supporting executable directives in federation [#3464](https://github.com/apollographql/apollo-server/pull/3464) + +## v0.10.8 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5d94e986f04457ec17114791ee6db3ece4213dd8) + +- Fix Gateway / Playground Query Plan view [#3418](https://github.com/apollographql/apollo-server/pull/3418) +- Gateway schema change listener bug + refactor [#3411](https://github.com/apollographql/apollo-server/pull/3411) introduces a change to the `experimental_didUpdateComposition` hook and `experimental_pollInterval` configuration behavior. + 1. Previously, the `experimental_didUpdateComposition` hook wouldn't be reliably called unless the `experimental_pollInterval` was set. If it _was_ called, it was sporadic and didn't necessarily mark the timing of an actual composition update. After this change, the hook is called on a successful composition update. + 2. The `experimental_pollInterval` configuration option now affects both the GCS polling interval when gateway is configured for managed federation, as well as the polling interval of services. The former being newly introduced behavior. +- Gateway cached DataSource bug [#3412](https://github.com/apollographql/apollo-server/pull/3412) introduces a fix for managed federation users where `DataSource`s wouldn't update correctly if a service's url changed. This bug was introduced with heavier DataSource caching in [#3388](https://github.com/apollographql/apollo-server/pull/3388). By inspecting the `url` as well, `DataSource`s will now update correctly when a composition update occurs. +- Gateway - don't log updates on startup [#3421](https://github.com/apollographql/apollo-server/pull/3421) Fine tune gateway startup logging - on load, instead of logging an "update", log the service id, variant, and mode in which gateway is running. + +## v0.10.7 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/fc7462ec5f8604bd6cba99aa9a377a9b8e045566) + +- Add export for experimental observability functions types. [#3371](https://github.com/apollographql/apollo-server/pull/3371) +- Fix double instantiation of DataSources [#3388](https://github.com/apollographql/apollo-server/pull/3388) + +## v0.10.6 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/aa200ce24b834320fc79d2605dac340b37d3e434) + +- Fix debug query plan logging [#3376](https://github.com/apollographql/apollo-server/pull/3376) +- Add `context` object to `GraphQLDataSource.didReceiveResponse` arguments [#3360](https://github.com/apollographql/apollo-server/pull/3360) + +## v0.10.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/029c8dca3af812ee70589cdb6de749df3d2843d8) + +- Make service definition cache local to ApolloGateway object [#3191](https://github.com/apollographql/apollo-server/pull/3191) +- Fix value type behavior within composition and execution [#3182](https://github.com/apollographql/apollo-server/pull/3182) +- Validate variables at the gateway level [#3213](https://github.com/apollographql/apollo-server/pull/3213) + +## v0.9.1 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a1c41152a35c837af27d1dee081fc273de07a28e) + +- Optimize buildQueryPlan when two FetchGroups are on the same service [#3135](https://github.com/apollographql/apollo-server/pull/3135) +- Construct and use RemoteGraphQLDataSource to issue introspection query to Federated Services [#3120](https://github.com/apollographql/apollo-server/pull/3120) + +## v0.9.0 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/99f78c6782bce170186ba6ef311182a8c9f281b7) + +- Add experimental observability functions [#3110](https://github.com/apollographql/apollo-server/pull/3110) + +## v0.8.2 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/b0a9ce0615d19b7241e64883b5d5d7730cc13fcb) + +- Handle `null` @requires selections correctly during execution [#3138](https://github.com/apollographql/apollo-server/pull/3138) + +## v0.6.13 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/a06594117dbbf1e8abdb7b366b69a94ab808b065) + +- Proxy errors from downstream services [#3019](https://github.com/apollographql/apollo-server/pull/3019) +- Handle schema defaultVariables correctly within downstream fetches [#2963](https://github.com/apollographql/apollo-server/pull/2963) + +## v0.6.12 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/5974b2ce405a06bc331230400b9073f6381738d3) + +- Fix `@requires` bug preventing array and null values. [PR #2928](https://github.com/apollographql/apollo-server/pull/2928) + +## v0.6.5 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/9dcfe6f91fa7b4187a644efe1522cf444ffc1251) + +- Relax constraints of root operation type names in validation [#2783](ttps://github.com/apollographql/apollo-server/pull/2783) + +## v0.6.2 + +> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/e113127b1ff9802de3bc5574bcae55256f0ef656) + +- Resolve an issue with \__proto__ pollution in deepMerge() [#2779](https://github.com/apollographql/apollo-server/pull/2779) diff --git a/gateway-js/LICENSE.md b/gateway-js/LICENSE.md new file mode 100644 index 000000000..c177b435f --- /dev/null +++ b/gateway-js/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2020- Apollo Graph, Inc. +Copyright (c) 2019-2020 Meteor Development Group, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gateway-js/README.md b/gateway-js/README.md new file mode 100644 index 000000000..e0bb61fd1 --- /dev/null +++ b/gateway-js/README.md @@ -0,0 +1,27 @@ +# Apollo Gateway + +This package provides utilities for combining multiple GraphQL microservices into a single GraphQL endpoint. + +Each microservice should implement the [federation schema specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). This can be done either through [Apollo Federation](https://github.com/apollographql/federation/tree/HEAD/federation-js) or a variety of other open source products. + +For complete documentation, see the [Apollo Gateway API reference](https://www.apollographql.com/docs/apollo-server/api/apollo-gateway/). + +## Usage + +```js +const { ApolloServer } = require("apollo-server"); +const { ApolloGateway } = require("@apollo/gateway"); + +const gateway = new ApolloGateway({ + serviceList: [ + { name: "accounts", url: "http://localhost:4001/graphql" }, + // List of federation-capable GraphQL endpoints... + ] +}); + +const server = new ApolloServer({ gateway }); + +server.listen().then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); +}); +``` diff --git a/gateway-js/jest.config.js b/gateway-js/jest.config.js new file mode 100644 index 000000000..9d165203d --- /dev/null +++ b/gateway-js/jest.config.js @@ -0,0 +1,11 @@ +const path = require('path'); +const config = require('../jest.config.base'); + +const additionalConfig = { + setupFilesAfterEnv: [path.resolve(__dirname, './src/__tests__/testSetup.ts')], + testPathIgnorePatterns: [ + ...config.testPathIgnorePatterns, + ] +}; + +module.exports = Object.assign(Object.create(null), config, additionalConfig); diff --git a/gateway-js/package.json b/gateway-js/package.json new file mode 100644 index 000000000..367407670 --- /dev/null +++ b/gateway-js/package.json @@ -0,0 +1,39 @@ +{ + "name": "@apollo/gateway", + "version": "0.20.1-alpha.0", + "description": "Apollo Gateway", + "author": "Apollo ", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/apollographql/federation.git", + "directory": "gateway-js/" + }, + "keywords": ["graphql", "federation", "gateway", "server", "apollo"], + "engines": { + "node": ">=12.13.0 <15.0" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@apollo/federation": "file:../federation-js", + "@apollo/query-planner-wasm": "0.0.2", + "@types/node-fetch": "2.5.4", + "apollo-engine-reporting-protobuf": "^0.5.2", + "apollo-graphql": "^0.6.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-core": "^2.17.0", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-types": "^0.5.1", + "loglevel": "^1.6.1", + "make-fetch-happen": "^8.0.0", + "pretty-format": "^26.0.0" + }, + "peerDependencies": { + "graphql": "^14.5.0 || ^15.0.0" + } +} diff --git a/gateway-js/src/QueryPlan.ts b/gateway-js/src/QueryPlan.ts new file mode 100644 index 000000000..28b37fa5f --- /dev/null +++ b/gateway-js/src/QueryPlan.ts @@ -0,0 +1,127 @@ +import { + FragmentDefinitionNode, + OperationDefinitionNode, + Kind, + SelectionNode as GraphQLJSSelectionNode, +} from 'graphql'; +import prettyFormat from 'pretty-format'; +import { queryPlanSerializer, astSerializer } from './snapshotSerializers'; +import { ComposedGraphQLSchema } from '@apollo/federation'; + +export type ResponsePath = (string | number)[]; + +export type WasmPointer = number; + +type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode }; + +export type OperationContext = { + schema: ComposedGraphQLSchema; + operation: OperationDefinitionNode; + fragments: FragmentMap; + queryPlannerPointer: WasmPointer; + operationString: string; +}; + +export interface QueryPlan { + kind: 'QueryPlan'; + node?: PlanNode; +} + +export type PlanNode = SequenceNode | ParallelNode | FetchNode | FlattenNode; + +export interface SequenceNode { + kind: 'Sequence'; + nodes: PlanNode[]; +} + +export interface ParallelNode { + kind: 'Parallel'; + nodes: PlanNode[]; +} + +export interface FetchNode { + kind: 'Fetch'; + serviceName: string; + variableUsages?: string[]; + requires?: QueryPlanSelectionNode[]; + operation: string; +} + +export interface FlattenNode { + kind: 'Flatten'; + path: ResponsePath; + node: PlanNode; +} + +/** + * SelectionNodes from GraphQL-js _can_ have a FragmentSpreadNode + * but this SelectionNode is specifically typing the `requires` key + * in a built query plan, where there can't be FragmentSpreadNodes + * since that info is contained in the `FetchNode.operation` + */ +export type QueryPlanSelectionNode = QueryPlanFieldNode | QueryPlanInlineFragmentNode; + +export interface QueryPlanFieldNode { + readonly kind: 'Field'; + readonly alias?: string; + readonly name: string; + readonly selections?: QueryPlanSelectionNode[]; +} + +export interface QueryPlanInlineFragmentNode { + readonly kind: 'InlineFragment'; + readonly typeCondition?: string; + readonly selections: QueryPlanSelectionNode[]; +} + +export function serializeQueryPlan(queryPlan: QueryPlan) { + return prettyFormat(queryPlan, { + plugins: [queryPlanSerializer, astSerializer], + }); +} + +export function getResponseName(node: QueryPlanFieldNode): string { + return node.alias ? node.alias : node.name; +} + +/** + * Converts a GraphQL-js SelectionNode to our newly defined SelectionNode + * + * This function is used to remove the unneeded pieces of a SelectionSet's + * `.selections`. It is only ever called on a query plan's `requires` field, + * so we can guarantee there won't be any FragmentSpreads passed in. That's why + * we can ignore the case where `selection.kind === Kind.FRAGMENT_SPREAD` + */ +export const trimSelectionNodes = ( + selections: readonly GraphQLJSSelectionNode[], +): QueryPlanSelectionNode[] => { + /** + * Using an array to push to instead of returning value from `selections.map` + * because TypeScript thinks we can encounter a `Kind.FRAGMENT_SPREAD` here, + * so if we mapped the array directly to the return, we'd have to `return undefined` + * from one branch of the map and then `.filter(Boolean)` on that returned + * array + */ + const remapped: QueryPlanSelectionNode[] = []; + + selections.forEach((selection) => { + if (selection.kind === Kind.FIELD) { + remapped.push({ + kind: Kind.FIELD, + name: selection.name.value, + selections: + selection.selectionSet && + trimSelectionNodes(selection.selectionSet.selections), + }); + } + if (selection.kind === Kind.INLINE_FRAGMENT) { + remapped.push({ + kind: Kind.INLINE_FRAGMENT, + typeCondition: selection.typeCondition?.name.value, + selections: trimSelectionNodes(selection.selectionSet.selections), + }); + } + }); + + return remapped; +}; diff --git a/gateway-js/src/__mocks__/apollo-server-env.ts b/gateway-js/src/__mocks__/apollo-server-env.ts new file mode 100644 index 000000000..0de1e052b --- /dev/null +++ b/gateway-js/src/__mocks__/apollo-server-env.ts @@ -0,0 +1,61 @@ +/// + +import { + fetch, + Request, + RequestInit, + Response, + Body, + BodyInit, + Headers, + HeadersInit, + URL, + URLSearchParams, + URLSearchParamsInit, +} from 'apollo-server-env'; + +interface FetchMock extends jest.Mock { + mockResponseOnce(data?: any, headers?: HeadersInit, status?: number): this; + mockJSONResponseOnce(data?: object, headers?: HeadersInit): this; +} + +const mockFetch = jest.fn(fetch) 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 = env; diff --git a/gateway-js/src/__tests__/.gitkeep b/gateway-js/src/__tests__/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gateway-js/src/__tests__/CucumberREADME.md b/gateway-js/src/__tests__/CucumberREADME.md new file mode 100644 index 000000000..25d4dc904 --- /dev/null +++ b/gateway-js/src/__tests__/CucumberREADME.md @@ -0,0 +1,96 @@ +# Query Plan Tests + +## Introduction + +There are two files used to test the query plan builder: + +1. [build-query-plan.feature](./build-query-plan.feature): Programming-language agnostic files written in a format called [Gherkin](https://cucumber.io/docs/gherkin/reference/) for [Cucumber](https://cucumber.io/). +2. [queryPlanCucumber.test.ts](./queryPlanCucumber.test.ts): The implementation which provides coverage for the Gherkin-specified behavior. + +> If you're not familiar with Cucumber or BDD, check out [this video](https://youtu.be/lC0jzd8sGIA) for a great introduction to the concepts involved. Cucumber has test runners in multiple languages, allowing a test spec to be written in plain English and then individual implementations of the test suite can describe how they would like tests to be run for their specific implementation. For Java, Kotlin, Ruby, and JavaScript, Cucumber even has a [10-minute tutorial](https://cucumber.io/docs/guides/10-minute-tutorial/) to help get started. + + +## Scenarios + +_Scenarios_ are Cucumber's test cases. Each scenario should contain the instructions for a single kind of test. + +## Steps + +Cucumber tests (scenarios) are made up of `steps`. Each step can be prefixed with a "`Given`", "`When`", or "`Then`" step, which when all provided, must occur in precisely that order. These stages represent test _preconditions_, test _execution_, and test _expectations_, respectively. However, tests don't _need_ all 3 of steps! Scenarios can leave off the `When` step when it's not needed. For example, query plan builder tests only have the "Given" and "Then" steps, like so: + +```gherkin +Scenario: should not confuse union types with overlapping field names + Given query + """ + query { + body { + ...on Image { + attributes { + url + } + } + ...on Text { + attributes { + bold + text + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "documents", + "variableUsages": [], + "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold text}}}}" + } + } + """ +``` + +There can be multiple of any kind of step using the `And` keyword. In the following example, there are 2 `Given` steps. One represented by the `Given` keyword itself, and another represented with the `And` keyword. + +``` +Given schema A +And schema extension B +Then composed schema should be ... +``` + +Using `And` is especially useful in `Then` steps for testing multiple kinds of expectations. For example, to create a test that looked at a query plan and expected that it called service A and _didn't_ call service B, the test spec would look like this: + +``` +Given service A, B +When querying + """ + query { a } + """ +Then calls service A +And doesn't call service B +``` + +## Writing test integrations + +Cucumber has a test runner for [many different languages](https://cucumber.io/docs/tools/related-tools/) and test frameworks including Java, Ruby, Rust, and many more. Usually, writing an integration for Cucumber looks similar though. You typically need to write instructions for what to with each kind of step. For example, in the example above where querying a service and expecting things of the query plan, we'd need to define 4 different kind of steps, typically with regex matchers (which are simplified here a bit): + +1. `^service *` +2. `^querying` +3. `^calls *` +4. `^doesn't call *` + +Using regex groups, we can extract whatever data we need from the test instructions. For the first pattern, we can use regex to get the service names we want to compose from the given list, and compose them based off a predetermined set of fixtures. + +Gherkin (the language Cucumber tests are written in) has the idea of [arguments](https://cucumber.io/docs/gherkin/reference/#step-arguments) as well, which is what is used in the second step (the `querying...`) step. The query `query { a }` is referred to as an argument to that step, and each cucumber runner has a way of handling arguments, usually as an argument to the handling function. + +In JavaScript, writing a function to handle the `querying` step would look something like this: + +```JavaScript +when(/^querying$/im, (operation) => { + result = execute(services, { query: gql(operation) }); +}); +``` + +It's common in Cucumber execution to keep arguments, variables, and other data globally available to each step. This is either done by a variable scoped above the execution of the steps like in the JavaScript example above or as a mutable "context" passed to each step executor function. This just depends on the language you're working with. The reason this pattern is used is that all steps often need similar data. For example, the `querying` step we defined above needs to know what services are being composed from the `Given` step above to actually execute the operation, and the `Then` steps to follow need to access the execution's result data. diff --git a/gateway-js/src/__tests__/build-query-plan-fragmentization.feature b/gateway-js/src/__tests__/build-query-plan-fragmentization.feature new file mode 100644 index 000000000..5cd58be22 --- /dev/null +++ b/gateway-js/src/__tests__/build-query-plan-fragmentization.feature @@ -0,0 +1,282 @@ +Feature: Build Query Plan > Auto-fragmentization + +Scenario: experimental compression to downstream services should generate fragments internally to downstream requests + Given query + """ + query { + topReviews { + body + author + product { + name + price + details { + country + } + } + } + } + """ + When using autofragmentization + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{...__QueryPlanFragment_1__}}fragment __QueryPlanFragment_1__ on Review{body author product{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["topReviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name price details{country}}...on Book{price details{country}}}}" + } + } + ] + } + ] + } + } + """ + +Scenario: experimental compression to downstream services shouldn't generate fragments for selection sets of length 2 or less + Given query + """ + query { + topReviews { + body + author + } + } + """ + When using autofragmentization + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author}}" + } + } + """ + +Scenario: experimental compression to downstream services should generate fragments for selection sets of length 3 or greater + Given query + """ + query { + topReviews { + id + body + author + } + } + """ + When using autofragmentization + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Review{id body author}" + } + } + """ + +Scenario: experimental compression to downstream services should generate fragments correctly when aliases are used + Given query + """ + query { + reviews: topReviews { + content: body + author + product { + name + cost: price + details { + origin: country + } + } + } + } + """ + When using autofragmentization + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{reviews:topReviews{...__QueryPlanFragment_1__}}fragment __QueryPlanFragment_1__ on Review{content:body author product{...__QueryPlanFragment_0__}}fragment __QueryPlanFragment_0__ on Product{__typename ...on Book{__typename isbn}...on Furniture{__typename upc}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + }, + { + "kind": "Flatten", + "path": ["reviews", "@", "product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name cost:price details{origin:country}}...on Book{cost:price details{origin:country}}}}" + } + } + ] + } + ] + } + } + """ diff --git a/gateway-js/src/__tests__/build-query-plan.feature b/gateway-js/src/__tests__/build-query-plan.feature new file mode 100644 index 000000000..41e2aa84d --- /dev/null +++ b/gateway-js/src/__tests__/build-query-plan.feature @@ -0,0 +1,1393 @@ +Feature: Build Query Plan + +Scenario: should not confuse union types with overlapping field names + Given query + """ + query { + body { + ...on Image { + attributes { + url + } + } + ...on Text { + attributes { + bold + text + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "documents", + "variableUsages": [], + "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold text}}}}" + } + } + """ + +Scenario: should use a single fetch when requesting a root field from one service + Given query + """ + query { + me { + name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{name}}" + } + } + """ + +Scenario: should use two independent fetches when requesting root fields from two services + Given query + """ + query { + me { + name + } + topProducts { + name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Parallel", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{name}}" + }, + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topProducts{__typename ...on Book{__typename isbn}...on Furniture{name}}}" + }, + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + ] + } + } + """ + +Scenario: should use a single fetch when requesting multiple root fields from the same service + Given query + """ + query { + topProducts { + name + } + product(upc: "1") { + name + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topProducts{__typename ...on Book{__typename isbn}...on Furniture{name}}product(upc:\"1\"){__typename ...on Book{__typename isbn}...on Furniture{name}}}" + }, + { + "kind": "Parallel", + "nodes": [ + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + }, + { + "kind": "Sequence", + "nodes": [ + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "books", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{__typename isbn title year}}}" + } + }, + { + "kind": "Flatten", + "path": ["product"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + ] + } + ] + } + } + """ + +Scenario: should use a single fetch when requesting relationship subfields from the same service + Given query + """ + query { + topReviews { + body + author { + reviews { + body + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author{reviews{body}}}}" + } + } + """ + +Scenario: should use a single fetch when requesting relationship subfields and provided keys from the same service + Given query + """ + query { + topReviews { + body + author { + id + reviews { + body + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author{id reviews{body}}}}" + } + } + """ + +Scenario: when requesting an extension field from another service, it should add the field's representation requirements to the parent selection set and use a dependent fetch + Given query + """ + query { + me { + name + reviews { + body + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{name __typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}" + } + } + ] + } + } + """ + +Scenario: when requesting an extension field from another service, when the parent selection set is empty, should add the field's requirements to the parent selection set and use a dependent fetch + Given query + """ + query { + me { + reviews { + body + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}}}}" + } + } + ] + } + } + """ + +Scenario: when requesting an extension field from another service, should only add requirements once + Given query + """ + query { + me { + reviews { + body + } + numberOfReviews + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [], + "operation": "{me{__typename id}}" + }, + { + "kind": "Flatten", + "path": ["me"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{body}numberOfReviews}}}" + } + } + ] + } + } + """ + +Scenario: when requesting a composite field with subfields from another service, it should add key fields to the parent selection set and use a dependent fetch + Given query + """ + query { + topReviews { + body + author { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{body author{__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + +Scenario: when requesting a composite field with subfields from another service, when requesting a field defined in another service which requires a field in the base service, it should add the field provided by base service in first Fetch + Given query + """ + query { + topCars { + retailPrice + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topCars{__typename id price}}" + }, + { + "kind": "Flatten", + "path": ["topCars", "@"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Car", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" }, + { "kind": "Field", "name": "price" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Car{retailPrice}}}" + } + } + ] + } + } + """ + +Scenario: when requesting a composite field with subfields from another service, when the parent selection set is empty, it should add key fields to the parent selection set and use a dependent fetch + Given query + """ + query { + topReviews { + author { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{author{__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}" + } + } + ] + } + } + """ + +Scenario: when requesting a relationship field with extension subfields from a different service, it should first fetch the object using a key from the base service and then pass through the requirements + Given query + """ + query { + topReviews { + author { + birthDate + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [], + "operation": "{topReviews{author{__typename id}}}" + }, + { + "kind": "Flatten", + "path": ["topReviews", "@", "author"], + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "id" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{birthDate}}}" + } + } + ] + } + } + """ + +Scenario: for abstract types, it should add __typename when fetching objects of an interface type from a service + Given query + """ + query { + topProducts { + price + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topProducts{__typename ...on Book{price}...on Furniture{price}}}" + } + } + """ + +Scenario: should break up when traversing an extension field on an interface type from a service + Given query + """ + query { + topProducts { + price + reviews { + body + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{topProducts{__typename ...on Book{price __typename isbn}...on Furniture{price __typename upc}}}" + }, + { + "kind": "Flatten", + "path": ["topProducts", "@"], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" } + ] + }, + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "upc" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{reviews{body}}...on Furniture{reviews{body}}}}" + } + } + ] + } + } + """ + +Scenario: interface fragments should expand into possible types only + Given query + """ + query { + books { + ... on Product { + name + ... on Furniture { + upc + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "books", + "variableUsages": [], + "operation": "{books{__typename isbn title year}}" + }, + { + "kind": "Flatten", + "path": ["books", "@"], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { "kind": "Field", "name": "__typename" }, + { "kind": "Field", "name": "isbn" }, + { "kind": "Field", "name": "title" }, + { "kind": "Field", "name": "year" } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{name}}}" + } + } + ] + } + } + """ + +Scenario: interface inside interface should expand into possible types only + Given query + """ + query { + product(upc: "") { + details { + country + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "product", + "variableUsages": [], + "operation": "{product(upc:\"\"){__typename ...on Book{details{country}}...on Furniture{details{country}}}}" + } + } + """ + +Scenario: should properly expand nested unions with inline fragments + Given query + """ + query { + body { + ... on Image { + ... on Body { + ... on Image { + attributes { + url + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + ... on Text { + attributes { + bold + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "documents", + "variableUsages": [], + "operation": "{body{__typename ...on Image{attributes{url}}...on Text{attributes{bold}}}}" + } + } + """ + +Scenario: deduplicates fields / selections regardless of adjacency and type condition nesting for inline fragments + Given query + """ + query { + body { + ... on Image { + ... on Text { + attributes { + bold + } + } + } + ... on Body { + ... on Text { + attributes { + bold + text + } + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "documents", + "variableUsages": [], + "operation": "{body{__typename ...on Text{attributes{bold text}}}}" + } + } + """ + +Scenario: deduplicates fields / selections regardless of adjacency and type condition nesting for named fragment spreads + Given query + """ + fragment TextFragment on Text { + attributes { + bold + text + } + } + + query { + body { + ... on Image { + ...TextFragment + } + ... on Body { + ...TextFragment + } + ...TextFragment + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "documents", + "variableUsages": [], + "operation": "{body{__typename ...on Text{attributes{bold text}}}}" + } + } + """ + +Scenario: supports basic, single-service mutation + Given query + """ + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + id + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [ + "username", + "password" + ], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){id}}" + } + } + """ + +# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L13 +Scenario: supports mutations with a cross-service request + Given query + """ + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [ + "username", + "password" + ], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": [ + "login" + ], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": [ + "login", + "reviews", + "@", + "product" + ], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "isbn" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + } + ] + } + } + """ + +# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L48 +Scenario: returning across service boundaries + Given query + """ + mutation Review($upc: String!, $body: String!) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [ + "upc", + "body" + ], + "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" + }, + { + "kind": "Flatten", + "path": [ + "reviewProduct" + ], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "upc" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" + } + } + ] + } + } + """ + +# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L75 +Scenario: supports multiple root mutations + Given query + """ + mutation LoginAndReview( + $username: String! + $password: String! + $upc: String! + $body: String! + ) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [ + "username", + "password" + ], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": [ + "login" + ], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": [ + "login", + "reviews", + "@", + "product" + ], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "isbn" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + }, + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [ + "upc", + "body" + ], + "operation": "mutation($upc:String!$body:String!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{__typename upc}}}" + }, + { + "kind": "Flatten", + "path": [ + "reviewProduct" + ], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Furniture", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "upc" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Furniture{name}}}" + } + } + ] + } + } + """ + +# ported from: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-gateway/src/__tests__/integration/mutations.test.ts#L136 +Scenario: multiple root mutations with correct service order + Given query + """ + mutation LoginAndReview( + $upc: String! + $body: String! + $updatedReview: UpdateReviewInput! + $username: String! + $password: String! + $reviewId: ID! + ) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + upc + } + } + updateReview(review: $updatedReview) { + id + body + } + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + deleteReview(id: $reviewId) + } + """ + Then query plan + """ + { + "kind": "QueryPlan", + "node": { + "kind": "Sequence", + "nodes": [ + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [ + "upc", + "body", + "updatedReview" + ], + "operation": "mutation($upc:String!$body:String!$updatedReview:UpdateReviewInput!){reviewProduct(upc:$upc body:$body){__typename ...on Furniture{upc}}updateReview(review:$updatedReview){id body}}" + }, + { + "kind": "Fetch", + "serviceName": "accounts", + "variableUsages": [ + "username", + "password" + ], + "operation": "mutation($username:String!$password:String!){login(username:$username password:$password){__typename id}}" + }, + { + "kind": "Flatten", + "path": [ + "login" + ], + "node": { + "kind": "Fetch", + "serviceName": "reviews", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "User", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "id" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{product{__typename ...on Book{__typename isbn}...on Furniture{upc}}}}}}" + } + }, + { + "kind": "Flatten", + "path": [ + "login", + "reviews", + "@", + "product" + ], + "node": { + "kind": "Fetch", + "serviceName": "product", + "requires": [ + { + "kind": "InlineFragment", + "typeCondition": "Book", + "selections": [ + { + "kind": "Field", + "name": "__typename" + }, + { + "kind": "Field", + "name": "isbn" + } + ] + } + ], + "variableUsages": [], + "operation": "query($representations:[_Any!]!){_entities(representations:$representations){...on Book{upc}}}" + } + }, + { + "kind": "Fetch", + "serviceName": "reviews", + "variableUsages": [ + "reviewId" + ], + "operation": "mutation($reviewId:ID!){deleteReview(id:$reviewId)}" + } + ] + } + } + """ diff --git a/gateway-js/src/__tests__/buildQueryPlan.test.ts b/gateway-js/src/__tests__/buildQueryPlan.test.ts new file mode 100644 index 000000000..736fd53af --- /dev/null +++ b/gateway-js/src/__tests__/buildQueryPlan.test.ts @@ -0,0 +1,1557 @@ +import { GraphQLError } from 'graphql'; +import gql from 'graphql-tag'; +import { buildQueryPlan, buildOperationContext } from '../buildQueryPlan'; +import { astSerializer, queryPlanSerializer } from '../snapshotSerializers'; +import { getFederatedTestingSchema } from './execution-utils'; +import { ComposedGraphQLSchema } from '@apollo/federation'; +import { WasmPointer } from '../QueryPlan'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +describe('buildQueryPlan', () => { + let schema: ComposedGraphQLSchema; + let errors: GraphQLError[]; + let queryPlannerPointer: WasmPointer; + + beforeEach(() => { + ({ schema, errors, queryPlannerPointer } = getFederatedTestingSchema()); + expect(errors).toHaveLength(0); + }); + + it(`should not confuse union types with overlapping field names`, () => { + const operationString = `#graphql + query { + body { + ... on Image { + attributes { + url + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "documents") { + { + body { + __typename + ... on Image { + attributes { + url + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + }, + } + `); + }); + + it(`should use a single fetch when requesting a root field from one service`, () => { + const operationString = `#graphql + query { + me { + name + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "accounts") { + { + me { + name + } + } + }, + } + `); + }); + + it(`should use two independent fetches when requesting root fields from two services`, () => { + const operationString = `#graphql + query { + me { + name + } + topProducts { + name + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Parallel { + Fetch(service: "accounts") { + { + me { + name + } + } + }, + Sequence { + Fetch(service: "product") { + { + topProducts { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + name + } + } + } + }, + Flatten(path: "topProducts.@") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "topProducts.@") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + }, + } + `); + }); + + it(`should use a single fetch when requesting multiple root fields from the same service`, () => { + const operationString = `#graphql + query { + topProducts { + name + } + product(upc: "1") { + name + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + topProducts { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + name + } + } + product(upc: "1") { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + name + } + } + } + }, + Parallel { + Sequence { + Flatten(path: "topProducts.@") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "topProducts.@") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + Sequence { + Flatten(path: "product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + }, + }, + } + `); + }); + + it(`should use a single fetch when requesting relationship subfields from the same service`, () => { + const operationString = `#graphql + query { + topReviews { + body + author { + reviews { + body + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "reviews") { + { + topReviews { + body + author { + reviews { + body + } + } + } + } + }, + } + `); + }); + + it(`should use a single fetch when requesting relationship subfields and provided keys from the same service`, () => { + const operationString = `#graphql + query { + topReviews { + body + author { + id + reviews { + body + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "reviews") { + { + topReviews { + body + author { + id + reviews { + body + } + } + } + } + }, + } + `); + }); + + describe(`when requesting an extension field from another service`, () => { + it(`should add the field's representation requirements to the parent selection set and use a dependent fetch`, () => { + const operationString = `#graphql + query { + me { + name + reviews { + body + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + name + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + body + } + } + } + }, + }, + }, + } + `); + }); + + describe(`when the parent selection set is empty`, () => { + it(`should add the field's requirements to the parent selection set and use a dependent fetch`, () => { + const operationString = `#graphql + query { + me { + reviews { + body + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + body + } + } + } + }, + }, + }, + } + `); + }); + }); + + // TODO: Ask martijn about the meaning of this test + it(`should only add requirements once`, () => { + const operationString = `#graphql + query { + me { + reviews { + body + } + numberOfReviews + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + body + } + numberOfReviews + } + } + }, + }, + }, + } + `); + }); + }); + + describe(`when requesting a composite field with subfields from another service`, () => { + it(`should add key fields to the parent selection set and use a dependent fetch`, () => { + const operationString = `#graphql + query { + topReviews { + body + author { + name + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + topReviews { + body + author { + __typename + id + } + } + } + }, + Flatten(path: "topReviews.@.author") { + Fetch(service: "accounts") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + } + } + }, + }, + }, + } + `); + }); + + describe(`when requesting a field defined in another service which requires a field in the base service`, () => { + it(`should add the field provided by base service in first Fetch`, () => { + const operationString = `#graphql + query { + topCars { + retailPrice + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + topCars { + __typename + id + price + } + } + }, + Flatten(path: "topCars.@") { + Fetch(service: "reviews") { + { + ... on Car { + __typename + id + price + } + } => + { + ... on Car { + retailPrice + } + } + }, + }, + }, + } + `); + }); + }); + + describe(`when the parent selection set is empty`, () => { + it(`should add key fields to the parent selection set and use a dependent fetch`, () => { + const operationString = `#graphql + query { + topReviews { + author { + name + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + topReviews { + author { + __typename + id + } + } + } + }, + Flatten(path: "topReviews.@.author") { + Fetch(service: "accounts") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + } + } + }, + }, + }, + } + `); + }); + }); + }); + describe(`when requesting a relationship field with extension subfields from a different service`, () => { + it(`should first fetch the object using a key from the base service and then pass through the requirements`, () => { + const operationString = `#graphql + query { + topReviews { + author { + birthDate + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + topReviews { + author { + __typename + id + } + } + } + }, + Flatten(path: "topReviews.@.author") { + Fetch(service: "accounts") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + birthDate + } + } + }, + }, + }, + } + `); + }); + }); + + describe(`for abstract types`, () => { + // GraphQLError: Cannot query field "isbn" on type "Book" + // Probably an issue with extending / interfaces in composition. None of the fields from the base Book type + // are showing up in the resulting schema. + it(`should add __typename when fetching objects of an interface type from a service`, () => { + const operationString = `#graphql + query { + topProducts { + price + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "product") { + { + topProducts { + __typename + ... on Book { + price + } + ... on Furniture { + price + } + } + } + }, + } + `); + }); + }); + + // GraphQLError: Cannot query field "isbn" on type "Book" + // Probably an issue with extending / interfaces in composition. None of the fields from the base Book type + // are showing up in the resulting schema. + it(`should break up when traversing an extension field on an interface type from a service`, () => { + const operationString = `#graphql + query { + topProducts { + price + reviews { + body + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + topProducts { + __typename + ... on Book { + price + __typename + isbn + } + ... on Furniture { + price + __typename + upc + } + } + } + }, + Flatten(path: "topProducts.@") { + Fetch(service: "reviews") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + reviews { + body + } + } + ... on Furniture { + reviews { + body + } + } + } + }, + }, + }, + } + `); + }); + + it(`interface fragments should expand into possible types only`, () => { + const operationString = `#graphql + query { + books { + ... on Product { + name + ... on Furniture { + upc + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "books") { + { + books { + __typename + isbn + title + year + } + } + }, + Flatten(path: "books.@") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + } + `); + }); + + it(`interface inside interface should expand into possible types only`, () => { + const operationString = `#graphql + query { + product(upc: "") { + details { + country + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "product") { + { + product(upc: "") { + __typename + ... on Book { + details { + country + } + } + ... on Furniture { + details { + country + } + } + } + } + }, + } + `); + }); + + describe.skip(`experimental compression to downstream services`, () => { + it(`should generate fragments internally to downstream requests`, () => { + const operationString = `#graphql + query { + topReviews { + body + author + product { + name + price + details { + country + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + { autoFragmentization: true }, + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + topReviews { + ...__QueryPlanFragment_1__ + } + } + fragment __QueryPlanFragment_1__ on Review { + body + author + product { + ...__QueryPlanFragment_0__ + } + } + fragment __QueryPlanFragment_0__ on Product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + }, + Parallel { + Sequence { + Flatten(path: "topReviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "topReviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + Flatten(path: "topReviews.@.product") { + Fetch(service: "product") { + { + ... on Furniture { + __typename + upc + } + ... on Book { + __typename + isbn + } + } => + { + ... on Furniture { + name + price + details { + country + } + } + ... on Book { + price + details { + country + } + } + } + }, + }, + }, + }, + } + `); + }); + + it(`shouldn't generate fragments for selection sets of length 2 or less`, () => { + const operationString = `#graphql + query { + topReviews { + body + author + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + { autoFragmentization: true }, + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "reviews") { + { + topReviews { + body + author + } + } + }, + } + `); + }); + + it(`should generate fragments for selection sets of length 3 or greater`, () => { + const operationString = `#graphql + query { + topReviews { + id + body + author + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + { autoFragmentization: true }, + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "reviews") { + { + topReviews { + ...__QueryPlanFragment_0__ + } + } + fragment __QueryPlanFragment_0__ on Review { + id + body + author + } + }, + } + `); + }); + + it(`should generate fragments correctly when aliases are used`, () => { + const operationString = `#graphql + query { + reviews: topReviews { + content: body + author + product { + name + cost: price + details { + origin: country + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + { autoFragmentization: true }, + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + reviews: topReviews { + ...__QueryPlanFragment_1__ + } + } + fragment __QueryPlanFragment_1__ on Review { + content: body + author + product { + ...__QueryPlanFragment_0__ + } + } + fragment __QueryPlanFragment_0__ on Product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + }, + Parallel { + Sequence { + Flatten(path: "reviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + Flatten(path: "reviews.@.product") { + Fetch(service: "product") { + { + ... on Furniture { + __typename + upc + } + ... on Book { + __typename + isbn + } + } => + { + ... on Furniture { + name + cost: price + details { + origin: country + } + } + ... on Book { + cost: price + details { + origin: country + } + } + } + }, + }, + }, + }, + } + `); + }); + }); + + it(`should properly expand nested unions with inline fragments`, () => { + const operationString = `#graphql + query { + body { + ... on Image { + ... on Body { + ... on Image { + attributes { + url + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + ... on Text { + attributes { + bold + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "documents") { + { + body { + __typename + ... on Image { + attributes { + url + } + } + ... on Text { + attributes { + bold + } + } + } + } + }, + } + `); + }); + + describe('deduplicates fields / selections regardless of adjacency and type condition nesting', () => { + it('for inline fragments', () => { + const operationString = `#graphql + query { + body { + ... on Image { + ... on Text { + attributes { + bold + } + } + } + ... on Body { + ... on Text { + attributes { + bold + text + } + } + } + ... on Text { + attributes { + bold + text + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "documents") { + { + body { + __typename + ... on Text { + attributes { + bold + text + } + } + } + } + }, + } + `); + }); + + it('for named fragment spreads', () => { + const operationString = `#graphql + fragment TextFragment on Text { + attributes { + bold + text + } + } + + query { + body { + ... on Image { + ...TextFragment + } + ... on Body { + ...TextFragment + } + ...TextFragment + } + } + `; + + const operationDocument = gql(operationString); + + const queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }) + ); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "documents") { + { + body { + __typename + ... on Text { + attributes { + bold + text + } + } + } + } + }, + } + `); + }); + }); +}); diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts new file mode 100644 index 000000000..600b635bc --- /dev/null +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -0,0 +1,831 @@ +import { GraphQLError, getIntrospectionQuery } from 'graphql'; +import { addResolversToSchema, GraphQLResolverMap } from 'apollo-graphql'; +import gql from 'graphql-tag'; +import { GraphQLRequestContext } from 'apollo-server-types'; +import { AuthenticationError } from 'apollo-server-core'; +import { ComposedGraphQLSchema } from '@apollo/federation'; +import { buildQueryPlan, buildOperationContext } from '../buildQueryPlan'; +import { executeQueryPlan } from '../executeQueryPlan'; +import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource'; +import { astSerializer, queryPlanSerializer } from '../snapshotSerializers'; +import { getFederatedTestingSchema } from './execution-utils'; +import { WasmPointer } from '../QueryPlan'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +describe('executeQueryPlan', () => { + let serviceMap: { + [serviceName: string]: LocalGraphQLDataSource; + }; + + function overrideResolversInService( + serviceName: string, + resolvers: GraphQLResolverMap, + ) { + addResolversToSchema(serviceMap[serviceName].schema, resolvers); + } + + let schema: ComposedGraphQLSchema; + let errors: GraphQLError[]; + let queryPlannerPointer: WasmPointer; + + beforeEach(() => { + ({ serviceMap, schema, errors, queryPlannerPointer } = getFederatedTestingSchema()); + expect(errors).toHaveLength(0); + }); + + function buildRequestContext(): GraphQLRequestContext { + return { + cache: undefined as any, + context: {}, + request: { + variables: {}, + }, + } as GraphQLRequestContext; + } + + describe(`errors`, () => { + it(`should not include an empty "errors" array when no errors were encountered`, async () => { + const operationString = `#graphql + query { + me { + name { + first + last + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response).not.toHaveProperty('errors'); + }); + + it(`should include an error when a root-level field errors out`, async () => { + overrideResolversInService('accounts', { + RootQuery: { + me() { + throw new AuthenticationError('Something went wrong'); + }, + }, + }); + + const operationString = `#graphql + query { + me { + name { + first + last + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response).toHaveProperty('data.me', null); + expect(response).toHaveProperty( + 'errors.0.message', + 'Something went wrong', + ); + expect(response).toHaveProperty( + 'errors.0.extensions.code', + 'UNAUTHENTICATED', + ); + expect(response).toHaveProperty( + 'errors.0.extensions.serviceName', + 'accounts', + ); + expect(response).toHaveProperty( + 'errors.0.extensions.query', + '{me{name{first last}}}', + ); + expect(response).toHaveProperty('errors.0.extensions.variables', {}); + }); + + it(`should still include other root-level results if one root-level field errors out`, async () => { + overrideResolversInService('accounts', { + RootQuery: { + me() { + throw new Error('Something went wrong'); + }, + }, + }); + + const operationString = `#graphql + query { + me { + name { + first + last + } + } + topReviews { + body + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response).toHaveProperty('data.me', null); + expect(response).toHaveProperty('data.topReviews', expect.any(Array)); + }); + + it(`should still include data from other services if one services is unavailable`, async () => { + delete serviceMap.accounts; + + const operationString = `#graphql + query { + me { + name { + first + last + } + } + topReviews { + body + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response).toHaveProperty('data.me', null); + expect(response).toHaveProperty('data.topReviews', expect.any(Array)); + }); + }); + + it(`should only return fields that have been requested directly`, async () => { + const operationString = `#graphql + query { + topReviews { + body + author { + name { + first + last + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "topReviews": Array [ + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Love it!", + }, + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Too expensive.", + }, + Object { + "author": Object { + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Could be better.", + }, + Object { + "author": Object { + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Prefer something else.", + }, + Object { + "author": Object { + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Wish I had read this before.", + }, + ], + } + `); + }); + + it('should not duplicate variable definitions', async () => { + const operationString = `#graphql + query Test($first: Int!) { + first: topReviews(first: $first) { + body + author { + name { + first + last + } + } + } + second: topReviews(first: $first) { + body + author { + name { + first + last + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const requestContext = buildRequestContext(); + requestContext.request.variables = { first: 3 }; + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + requestContext, + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "first": Array [ + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Love it!", + }, + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Too expensive.", + }, + Object { + "author": Object { + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Could be better.", + }, + ], + "second": Array [ + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Love it!", + }, + Object { + "author": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Too expensive.", + }, + Object { + "author": Object { + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Could be better.", + }, + ], + } + `); + }); + + it('should include variables in non-root requests', async () => { + const operationString = `#graphql + query Test($locale: String) { + topReviews { + body + author { + name { + first + last + } + birthDate(locale: $locale) + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const requestContext = buildRequestContext(); + requestContext.request.variables = { locale: 'en-US' }; + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + requestContext, + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "topReviews": Array [ + Object { + "author": Object { + "birthDate": "12/10/1815", + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Love it!", + }, + Object { + "author": Object { + "birthDate": "12/10/1815", + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "body": "Too expensive.", + }, + Object { + "author": Object { + "birthDate": "6/23/1912", + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Could be better.", + }, + Object { + "author": Object { + "birthDate": "6/23/1912", + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Prefer something else.", + }, + Object { + "author": Object { + "birthDate": "6/23/1912", + "name": Object { + "first": "Alan", + "last": "Turing", + }, + }, + "body": "Wish I had read this before.", + }, + ], + } + `); + }); + + it('can execute an introspection query', async () => { + const operationContext = buildOperationContext({ + schema, + operationDocument: gql` + ${getIntrospectionQuery()} + `, + operationString: getIntrospectionQuery(), + queryPlannerPointer, + }); + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toHaveProperty('__schema'); + expect(response.errors).toBeUndefined(); + }); + + it(`can execute queries on interface types`, async () => { + const operationString = `#graphql + query { + vehicle(id: "1") { + description + price + retailPrice + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "vehicle": Object { + "description": "Humble Toyota", + "price": "9990", + "retailPrice": "9990", + }, + } + `); + }); + + it(`can execute queries whose fields are interface types`, async () => { + const operationString = `#graphql + query { + user(id: "1") { + name { + first + last + } + vehicle { + description + price + retailPrice + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "user": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + "vehicle": Object { + "description": "Humble Toyota", + "price": "9990", + "retailPrice": "9990", + }, + }, + } + `); + }); + + it(`can execute queries whose fields are union types`, async () => { + const operationString = `#graphql + query { + user(id: "1") { + name { + first + last + } + thing { + ... on Vehicle { + description + price + retailPrice + } + ... on Ikea { + asile + } + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "user": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + "thing": Object { + "description": "Humble Toyota", + "price": "9990", + "retailPrice": "9990", + }, + }, + } + `); + }); + + it('can execute queries with falsey @requires (except undefined)', async () => { + const operationString = `#graphql + query { + books { + name # Requires title, year (on Book type) + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "books": Array [ + Object { + "name": "Structure and Interpretation of Computer Programs (1996)", + }, + Object { + "name": "Object Oriented Software Construction (1997)", + }, + Object { + "name": "Design Patterns (1995)", + }, + Object { + "name": "The Year Was Null (null)", + }, + Object { + "name": " (404)", + }, + Object { + "name": "No Books Like This Book! (2019)", + }, + ], + } + `); + }); + + it('can execute queries with list @requires', async () => { + const operationString = `#graphql + query { + book(isbn: "0201633612") { + # Requires similarBooks { isbn } + relatedReviews { + id + body + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.errors).toMatchInlineSnapshot(`undefined`); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "book": Object { + "relatedReviews": Array [ + Object { + "body": "A classic.", + "id": "6", + }, + Object { + "body": "A bit outdated.", + "id": "5", + }, + ], + }, + } + `); + }); + + it('can execute queries with selections on null @requires fields', async () => { + const operationString = `#graphql + query { + book(isbn: "0987654321") { + # Requires similarBooks { isbn } + relatedReviews { + id + body + } + } + } + `; + + const operationDocument = gql(operationString); + + const operationContext = buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + buildRequestContext(), + operationContext, + ); + + expect(response.errors).toBeUndefined(); + + expect(response.data).toMatchInlineSnapshot(` + Object { + "book": Object { + "relatedReviews": Array [], + }, + } + `); + }); +}); diff --git a/gateway-js/src/__tests__/execution-utils.ts b/gateway-js/src/__tests__/execution-utils.ts new file mode 100644 index 000000000..f59efd5f0 --- /dev/null +++ b/gateway-js/src/__tests__/execution-utils.ts @@ -0,0 +1,122 @@ +import { + GraphQLSchemaValidationError, + GraphQLSchemaModule, + GraphQLResolverMap, +} from 'apollo-graphql'; +import { GraphQLRequest, GraphQLExecutionResult, Logger } from 'apollo-server-types'; +import { + composeAndValidate, + buildFederatedSchema, + ServiceDefinition, +} from '@apollo/federation'; + +import { + buildQueryPlan, + executeQueryPlan, + QueryPlan, + buildOperationContext, +} from '@apollo/gateway'; +import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource'; +import { mergeDeep } from 'apollo-utilities'; + +import queryPlanSerializer from '../snapshotSerializers/queryPlanSerializer'; +import astSerializer from '../snapshotSerializers/astSerializer'; +import gql from 'graphql-tag'; +import { fixtures } from 'apollo-federation-integration-testsuite'; +import { getQueryPlanner } from '@apollo/query-planner-wasm'; + +const prettyFormat = require('pretty-format'); + +export type ServiceDefinitionModule = ServiceDefinition & GraphQLSchemaModule; + +export function overrideResolversInService( + module: ServiceDefinitionModule, + resolvers: GraphQLResolverMap, +): ServiceDefinitionModule { + return { + name: module.name, + typeDefs: module.typeDefs, + resolvers: mergeDeep(module.resolvers, resolvers), + }; +} + +export async function execute( + request: GraphQLRequest, + services: ServiceDefinitionModule[] = fixtures, + logger: Logger = console, +): Promise { + const serviceMap = Object.fromEntries( + services.map(({ name, typeDefs, resolvers }) => { + return [ + name, + new LocalGraphQLDataSource( + buildFederatedSchema([{ typeDefs, resolvers }]), + ), + ] as [string, LocalGraphQLDataSource]; + }), + ); + + const { schema, queryPlannerPointer } = getFederatedTestingSchema(services); + + const operationContext = buildOperationContext({ + schema, + operationDocument: gql`${request.query}`, + operationString: request.query!, + queryPlannerPointer, + }); + + const queryPlan = buildQueryPlan(operationContext); + + const result = await executeQueryPlan( + queryPlan, + serviceMap, + { + cache: undefined as any, + context: {}, + request, + logger + }, + operationContext, + ); + + return { ...result, queryPlan }; +} + +export function buildLocalService(modules: GraphQLSchemaModule[]) { + const schema = buildFederatedSchema(modules); + return new LocalGraphQLDataSource(schema); +} + +export function getFederatedTestingSchema(services: ServiceDefinitionModule[] = fixtures) { + const serviceMap = Object.fromEntries( + services.map((service) => [ + service.name, + buildLocalService([service]), + ]), + ); + + const { schema, errors, composedSdl } = composeAndValidate( + Object.entries(serviceMap).map(([serviceName, dataSource]) => ({ + name: serviceName, + typeDefs: dataSource.sdl(), + })), + ); + + if (errors && errors.length > 0) { + throw new GraphQLSchemaValidationError(errors); + } + + const queryPlannerPointer = getQueryPlanner(composedSdl!); + + return { serviceMap, schema, errors, queryPlannerPointer }; +} + +export function wait(ms: number) { + return new Promise(r => setTimeout(r, ms)); +} + +export function printPlan(queryPlan: QueryPlan): string { + return prettyFormat(queryPlan, { + plugins: [queryPlanSerializer, astSerializer], + }); +} diff --git a/gateway-js/src/__tests__/gateway/buildService.test.ts b/gateway-js/src/__tests__/gateway/buildService.test.ts new file mode 100644 index 000000000..1f08046bb --- /dev/null +++ b/gateway-js/src/__tests__/gateway/buildService.test.ts @@ -0,0 +1,250 @@ +import gql from 'graphql-tag'; +import { fetch } from '__mocks__/apollo-server-env'; +import { createTestClient } from 'apollo-server-testing'; +import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; + +import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource'; +import { ApolloGateway, SERVICE_DEFINITION_QUERY } from '../../'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +beforeEach(() => { + fetch.mockReset(); +}); + +it('calls buildService only once per service', async () => { + fetch.mockJSONResponseOnce({ + data: { _service: { sdl: `extend type Query { thing: String }` } }, + }); + + const buildServiceSpy = jest.fn(() => { + return new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + }); + }); + + const gateway = new ApolloGateway({ + serviceList: [{ name: 'foo', url: 'https://api.example.com/foo' }], + buildService: buildServiceSpy + }); + + await gateway.load(); + + expect(buildServiceSpy).toHaveBeenCalledTimes(1); +}); + +it('correctly passes the context from ApolloServer to datasources', async () => { + const gateway = new ApolloGateway({ + localServiceList: fixtures, + buildService: _service => { + return new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + willSendRequest: ({ request, context }) => { + request.http?.headers.set('x-user-id', context.userId); + }, + }); + }, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ + schema, + executor, + context: () => ({ + userId: '1234', + }), + }); + + const call = createTestClient(server); + + const query = gql` + { + me { + username + } + } + `; + + fetch.mockJSONResponseOnce({ data: { me: { username: '@jbaxleyiii' } } }); + + const result = await call.query({ + query, + }); + + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual({ + me: { username: '@jbaxleyiii' }, + }); + + expect(fetch).toBeCalledTimes(1); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { + query: `{me{username}}`, + variables: {}, + }, + headers: { + 'x-user-id': '1234', + }, + }); +}); + +function createSdlData(sdl: string): object { + return { + data: { + _service: { + sdl: sdl, + }, + }, + }; +} + +it('makes enhanced introspection request using datasource', async () => { + fetch.mockJSONResponseOnce( + createSdlData('extend type Query { one: String }'), + ); + + const gateway = new ApolloGateway({ + serviceList: [ + { + name: 'one', + url: 'https://api.example.com/one', + }, + ], + buildService: _service => { + return new RemoteGraphQLDataSource({ + url: 'https://api.example.com/override', + willSendRequest: ({ request }) => { + request.http?.headers.set('custom-header', 'some-custom-value'); + }, + }); + }, + }); + + await gateway.load(); + + expect(fetch).toBeCalledTimes(1); + + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/override', + body: { + query: SERVICE_DEFINITION_QUERY, + }, + headers: { + 'custom-header': 'some-custom-value', + }, + }); +}); + +it('customizes request on a per-service basis', async () => { + fetch + .mockJSONResponseOnce(createSdlData('extend type Query { one: String }')) + .mockJSONResponseOnce(createSdlData('extend type Query { two: String }')) + .mockJSONResponseOnce(createSdlData('extend type Query { three: String }')); + + const gateway = new ApolloGateway({ + serviceList: [ + { + name: 'one', + url: 'https://api.example.com/one', + }, + { + name: 'two', + url: 'https://api.example.com/two', + }, + { + name: 'three', + url: 'https://api.example.com/three', + }, + ], + buildService: service => { + return new RemoteGraphQLDataSource({ + url: service.url, + willSendRequest: ({ request }) => { + request.http?.headers.set('service-name', service.name); + }, + }); + }, + }); + + await gateway.load(); + + expect(fetch).toBeCalledTimes(3); + + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/one', + body: { + query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, + }, + headers: { + 'service-name': 'one', + }, + }); + + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/two', + body: { + query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, + }, + headers: { + 'service-name': 'two', + }, + }); + + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/three', + body: { + query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`, + }, + headers: { + 'service-name': 'three', + }, + }); +}); + +it('does not share service definition cache between gateways', async () => { + let updates = 0; + const updateObserver: any = (..._args: any[]) => { + updates += 1; + }; + + // Initialize first gateway + { + fetch.mockJSONResponseOnce( + createSdlData('extend type Query { repeat: String }'), + ); + + const gateway = new ApolloGateway({ + serviceList: [ + { + name: 'repeat', + url: 'https://api.example.com/repeat', + }, + ], + experimental_didUpdateComposition: updateObserver, + }); + + await gateway.load(); + } + + // Initialize second gateway + { + fetch.mockJSONResponseOnce( + createSdlData('extend type Query { repeat: String }'), + ); + + const gateway = new ApolloGateway({ + serviceList: [ + { + name: 'repeat', + url: 'https://api.example.com/repeat', + }, + ], + experimental_didUpdateComposition: updateObserver, + }); + + await gateway.load(); + } + + expect(updates).toEqual(2); +}); diff --git a/gateway-js/src/__tests__/gateway/executor.test.ts b/gateway-js/src/__tests__/gateway/executor.test.ts new file mode 100644 index 000000000..a2dba2e31 --- /dev/null +++ b/gateway-js/src/__tests__/gateway/executor.test.ts @@ -0,0 +1,90 @@ +import gql from 'graphql-tag'; +import { ApolloGateway } from '../../'; +import { ApolloServer } from "apollo-server"; +import { fixtures } from 'apollo-federation-integration-testsuite'; +import { Logger } from 'apollo-server-types'; + +let logger: Logger; + +beforeEach(() => { + const warn = jest.fn(); + const debug = jest.fn(); + const error = jest.fn(); + const info = jest.fn(); + + logger = { + warn, + debug, + error, + info, + }; +}); + +describe('ApolloGateway executor', () => { + it('validates requests prior to execution', async () => { + const gateway = new ApolloGateway({ + localServiceList: fixtures, + }); + + const { executor } = await gateway.load(); + + const source = `#graphql + query InvalidVariables($first: Int!) { + topReviews(first: $first) { + body + } + } + `; + + const { errors } = await executor({ + source, + document: gql(source), + request: { + variables: { first: '3' }, + }, + queryHash: 'hashed', + context: null, + cache: {} as any, + logger, + }); + + expect(errors![0].message).toMatch( + 'Variable "$first" got invalid value "3";', + ); + }); + + it('still sets the ApolloServer executor on load rejection', async () => { + const gateway = new ApolloGateway({ + // Empty service list will trigger the gateway to crash on load, which is what we want. + serviceList: [], + logger, + }); + + // Mock implementation of process.exit with another () => never function. + // This is because the gateway doesn't just throw in this scenario, it crashes. + const mockExit = jest + .spyOn(process, 'exit') + .mockImplementation((code) => { + throw new Error(code?.toString()); + }); + + const server = new ApolloServer({ + gateway, + subscriptions: false, + logger, + }); + + // Ensure the throw happens to maintain the correctness of this test. + await expect( + server.executeOperation({ query: '{ __typename }' })).rejects.toThrow(); + + expect(server.requestOptions.executor).toBe(gateway.executor); + + expect(logger.error.mock.calls).toEqual([ + ["Error checking for changes to service definitions: Tried to load services from remote endpoints but none provided"], + ["This data graph is missing a valid configuration. Tried to load services from remote endpoints but none provided"] + ]); + + mockExit.mockRestore(); + }); +}); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts new file mode 100644 index 000000000..0daba3aa5 --- /dev/null +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -0,0 +1,268 @@ +import gql from 'graphql-tag'; +import { + ApolloGateway, + GatewayConfig, + Experimental_DidResolveQueryPlanCallback, + Experimental_UpdateServiceDefinitions, +} from '../../index'; +import { + product, + reviews, + inventory, + accounts, + books, + documents, +} from 'apollo-federation-integration-testsuite'; +import { Logger } from 'apollo-server-types'; + +// The order of this was specified to preserve existing test coverage. Typically +// we would just import and use the `fixtures` array. +const serviceDefinitions = [ + product, + reviews, + inventory, + accounts, + books, + documents, +].map((s, i) => ({ + name: s.name, + typeDefs: s.typeDefs, + url: `http://localhost:${i}`, +})); + +let logger: Logger; + +beforeEach(() => { + const warn = jest.fn(); + const debug = jest.fn(); + const error = jest.fn(); + const info = jest.fn(); + + logger = { + warn, + debug, + error, + info, + }; +}); + +describe('lifecycle hooks', () => { + it('uses updateServiceDefinitions override', async () => { + const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( + async (_config: GatewayConfig) => { + return { serviceDefinitions, isNewSchema: true }; + }, + ); + + const gateway = new ApolloGateway({ + serviceList: serviceDefinitions, + experimental_updateServiceDefinitions, + experimental_didUpdateComposition: jest.fn(), + logger, + }); + + await gateway.load(); + + expect(experimental_updateServiceDefinitions).toBeCalled(); + expect(gateway.schema!.getType('Furniture')).toBeDefined(); + }); + + it('calls experimental_didFailComposition with a bad config', async () => { + const experimental_didFailComposition = jest.fn(); + + const gateway = new ApolloGateway({ + async experimental_updateServiceDefinitions(_config: GatewayConfig) { + return { + serviceDefinitions: [serviceDefinitions[0]], + compositionMetadata: { + formatVersion: 1, + id: 'abc', + implementingServiceLocations: [], + schemaHash: 'abc', + }, + isNewSchema: true, + }; + }, + serviceList: [], + experimental_didFailComposition, + logger, + }); + + await expect(gateway.load()).rejects.toThrowError(); + + const callbackArgs = experimental_didFailComposition.mock.calls[0][0]; + expect(callbackArgs.serviceList).toHaveLength(1); + expect(callbackArgs.errors[0]).toMatchInlineSnapshot( + `[GraphQLError: [product] Book -> \`Book\` is an extension type, but \`Book\` is not defined in any service]`, + ); + expect(callbackArgs.compositionMetadata.id).toEqual('abc'); + expect(experimental_didFailComposition).toBeCalled(); + }); + + it('calls experimental_didUpdateComposition on schema update', async () => { + const compositionMetadata = { + formatVersion: 1, + id: 'abc', + implementingServiceLocations: [], + schemaHash: 'hash1', + }; + + const update: Experimental_UpdateServiceDefinitions = async ( + _config: GatewayConfig, + ) => ({ + serviceDefinitions, + isNewSchema: true, + compositionMetadata: { + ...compositionMetadata, + id: '123', + schemaHash: 'hash2', + }, + }); + + // This is the simplest way I could find to achieve mocked functions that leverage our types + const mockUpdate = jest.fn(update); + + // We want to return a different composition across two ticks, so we mock it + // slightly differenty + mockUpdate.mockImplementationOnce(async (_config: GatewayConfig) => { + const services = serviceDefinitions.filter(s => s.name !== 'books'); + return { + serviceDefinitions: [ + ...services, + { + name: 'book', + typeDefs: books.typeDefs, + url: 'http://localhost:32542', + }, + ], + isNewSchema: true, + compositionMetadata, + }; + }); + + const mockDidUpdate = jest.fn(); + + const gateway = new ApolloGateway({ + experimental_updateServiceDefinitions: mockUpdate, + experimental_didUpdateComposition: mockDidUpdate, + logger, + }); + // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here + gateway.experimental_pollInterval = 100; + + let resolve1: Function; + let resolve2: Function; + const schemaChangeBlocker1 = new Promise(res => (resolve1 = res)); + const schemaChangeBlocker2 = new Promise(res => (resolve2 = res)); + + gateway.onSchemaChange( + jest + .fn() + .mockImplementationOnce(() => resolve1()) + .mockImplementationOnce(() => resolve2()), + ); + + await gateway.load(); + + await schemaChangeBlocker1; + expect(mockUpdate).toBeCalledTimes(1); + expect(mockDidUpdate).toBeCalledTimes(1); + + await schemaChangeBlocker2; + expect(mockUpdate).toBeCalledTimes(2); + expect(mockDidUpdate).toBeCalledTimes(2); + + const [firstCall, secondCall] = mockDidUpdate.mock.calls; + + expect(firstCall[0]!.schema).toBeDefined(); + expect(firstCall[0].compositionMetadata!.schemaHash).toEqual('hash1'); + // first call should have no second "previous" argument + expect(firstCall[1]).toBeUndefined(); + + expect(secondCall[0].schema).toBeDefined(); + expect(secondCall[0].compositionMetadata!.schemaHash).toEqual('hash2'); + // second call should have previous info in the second arg + expect(secondCall[1]!.schema).toBeDefined(); + expect(secondCall[1]!.compositionMetadata!.schemaHash).toEqual('hash1'); + }); + + it('uses default service definition updater', async () => { + const gateway = new ApolloGateway({ + localServiceList: serviceDefinitions, + logger, + }); + + const { schema } = await gateway.load(); + + // spying on gateway.loadServiceDefinitions wasn't working, so this also + // should test functionality. If there's no overwriting service definition + // updater, it has to use the default. If there's a valid schema, then + // the loader had to have been called. + expect(schema.getType('User')).toBeDefined(); + }); + + it('warns when polling on the default fetcher', async () => { + new ApolloGateway({ + serviceList: serviceDefinitions, + experimental_pollInterval: 10, + logger, + }); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith( + 'Polling running services is dangerous and not recommended in production. Polling should only be used against a registry. If you are polling running services, use with caution.', + ); + }); + + it('registers schema change callbacks when experimental_pollInterval is set for unmanaged configs', async () => { + const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions = jest.fn( + async (_config: GatewayConfig) => { + return { serviceDefinitions, isNewSchema: true }; + }, + ); + + const gateway = new ApolloGateway({ + serviceList: [{ name: 'book', url: 'http://localhost:32542' }], + experimental_updateServiceDefinitions, + experimental_pollInterval: 100, + logger, + }); + + let resolve: Function; + const schemaChangeBlocker = new Promise(res => (resolve = res)); + const schemaChangeCallback = jest.fn(() => resolve()); + + gateway.onSchemaChange(schemaChangeCallback); + gateway.load(); + + await schemaChangeBlocker; + + expect(schemaChangeCallback).toBeCalledTimes(1); + }); + + it('calls experimental_didResolveQueryPlan when executor is called', async () => { + const experimental_didResolveQueryPlan: Experimental_DidResolveQueryPlanCallback = jest.fn() + + const gateway = new ApolloGateway({ + localServiceList: [ + books + ], + experimental_didResolveQueryPlan, + }); + + const { executor } = await gateway.load(); + + const source = `#graphql + { book(isbn: "0262510871") { year } } + `; + + await executor({ + source, + document: gql(source), + request: {}, + queryHash: 'hashed', + context: {}, + }); + + expect(experimental_didResolveQueryPlan).toBeCalled(); + }); +}); diff --git a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts new file mode 100644 index 000000000..f1deddf65 --- /dev/null +++ b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts @@ -0,0 +1,220 @@ +import gql from 'graphql-tag'; +import { createTestClient } from 'apollo-server-testing'; +import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; +import { buildFederatedSchema } from '@apollo/federation'; + +import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource'; +import { ApolloGateway } from '../../'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +it('caches the query plan for a request', async () => { + const planner = require('../../buildQueryPlan'); + const originalPlanner = planner.buildQueryPlan; + + planner.buildQueryPlan = jest.fn(originalPlanner); + + const gateway = new ApolloGateway({ + localServiceList: fixtures, + buildService: service => { + return new LocalGraphQLDataSource(buildFederatedSchema([service])); + }, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ schema, executor }); + + const upc = '1'; + const call = createTestClient(server); + + const query = gql` + query GetProduct($upc: String!) { + product(upc: $upc) { + name + } + } + `; + + const result = await call.query({ + query, + variables: { upc }, + }); + + expect(result.data).toEqual({ + product: { + name: 'Table', + }, + }); + + const secondResult = await call.query({ + query, + variables: { upc }, + }); + + expect(result.data).toEqual(secondResult.data); + expect(planner.buildQueryPlan).toHaveBeenCalledTimes(1); +}); + +it('supports multiple operations and operationName', async () => { + const query = `#graphql + query GetUser { + me { + username + } + } + query GetReviews { + topReviews { + body + } + } + `; + + const gateway = new ApolloGateway({ + localServiceList: fixtures, + buildService: service => { + return new LocalGraphQLDataSource(buildFederatedSchema([service])); + }, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ schema, executor }); + + const { data: userData } = await server.executeOperation({ + query, + operationName: 'GetUser', + }); + + const { data: reviewsData } = await server.executeOperation({ + query, + operationName: 'GetReviews', + }); + + expect(userData).toEqual({ + me: { username: '@ada' }, + }); + expect(reviewsData).toEqual({ + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); +}); + +it('does not corrupt cached queryplan data across requests', async () => { + const serviceA = { + name: 'a', + typeDefs: gql` + type Query { + user: User + } + + type User @key(fields: "id") { + id: ID! + preferences: Preferences + } + + type Preferences { + favorites: Things + } + + type Things { + color: String + animal: String + } + `, + resolvers: { + Query: { + user() { + return { + id: '1', + preferences: { + favorites: { color: 'limegreen', animal: 'platypus' }, + }, + }; + }, + }, + }, + }; + + const serviceB = { + name: 'b', + typeDefs: gql` + extend type User @key(fields: "id") { + id: ID! @external + preferences: Preferences @external + favoriteColor: String + @requires(fields: "preferences { favorites { color } }") + favoriteAnimal: String + @requires(fields: "preferences { favorites { animal } }") + } + + extend type Preferences { + favorites: Things @external + } + + extend type Things { + color: String @external + animal: String @external + } + `, + resolvers: { + User: { + favoriteColor(user: any) { + return user.preferences.favorites.color; + }, + favoriteAnimal(user: any) { + return user.preferences.favorites.animal; + }, + }, + }, + }; + + const gateway = new ApolloGateway({ + localServiceList: [serviceA, serviceB], + buildService: service => { + return new LocalGraphQLDataSource(buildFederatedSchema([service])); + }, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ schema, executor }); + + const call = createTestClient(server); + + const query1 = `#graphql + query UserFavoriteColor { + user { + favoriteColor + } + } + `; + + const query2 = `#graphql + query UserFavorites { + user { + favoriteColor + favoriteAnimal + } + } + `; + + const result1 = await call.query({ + query: query1, + }); + const result2 = await call.query({ + query: query2, + }); + const result3 = await call.query({ + query: query1, + }); + + expect(result1.errors).toEqual(undefined); + expect(result2.errors).toEqual(undefined); + expect(result3.errors).toEqual(undefined); + expect(result1).toEqual(result3); +}); diff --git a/gateway-js/src/__tests__/gateway/reporting.test.ts b/gateway-js/src/__tests__/gateway/reporting.test.ts new file mode 100644 index 000000000..25f83b2c7 --- /dev/null +++ b/gateway-js/src/__tests__/gateway/reporting.test.ts @@ -0,0 +1,605 @@ +import { gunzipSync } from 'zlib'; +import nock from 'nock'; +import { GraphQLSchemaModule } from 'apollo-graphql'; +import gql from 'graphql-tag'; +import { buildFederatedSchema } from '@apollo/federation'; +import { ApolloServer } from 'apollo-server'; +import { execute, toPromise } from 'apollo-link'; +import { createHttpLink } from 'apollo-link-http'; +import fetch from 'node-fetch'; +import { ApolloGateway } from '../..'; +import { Plugin, Config, Refs } from 'pretty-format'; +import { Report } from 'apollo-engine-reporting-protobuf'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +// Normalize specific fields that change often (eg timestamps) to static values, +// to make snapshot testing viable. (If these helpers are more generally +// useful, they could be moved to a different file.) + +const alreadyProcessed = '__already_processed__'; + +function replaceFieldValuesSerializer( + replacements: Record, +): Plugin { + const fieldNames = Object.keys(replacements); + return { + test(value: any) { + return ( + value && + typeof value === 'object' && + !value[alreadyProcessed] && + fieldNames.some((n) => n in value) + ); + }, + + serialize( + value: Record, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: any, + ): string { + // Clone object so pretty-format doesn't consider it as a circular + // reference. Put a special (non-enumerable) property on it so that *we* + // don't reprocess it ourselves. + const newValue = { ...value }; + Object.defineProperty(newValue, alreadyProcessed, { value: true }); + fieldNames.forEach((fn) => { + if (fn in value) { + const replacement = replacements[fn]; + if (typeof replacement === 'function') { + newValue[fn] = replacement(value[fn]); + } else { + newValue[fn] = replacement; + } + } + }); + return printer(newValue, config, indentation, depth, refs, printer); + }, + }; +} + +expect.addSnapshotSerializer( + replaceFieldValuesSerializer({ + header: '
', + // We do want to differentiate between zero and non-zero in these numbers. + durationNs: (v: number) => (v ? 12345 : 0), + sentTimeOffset: (v: number) => (v ? 23456 : 0), + // endTime and startTime are annoyingly used both for top-level Timestamps + // and for node-level nanosecond offsets. The Timestamps will get normalized + // by the nanos/seconds below. + startTime: (v: any) => (typeof v === 'string' ? '34567' : v), + endTime: (v: any) => (typeof v === 'string' ? '45678' : v), + nanos: 123000000, + seconds: '1562203363', + }), +); + +async function startFederatedServer(modules: GraphQLSchemaModule[]) { + const schema = buildFederatedSchema(modules); + const server = new ApolloServer({ schema }); + const { url } = await server.listen({ port: 0 }); + return { url, server }; +} + +describe('reporting', () => { + let backendServers: ApolloServer[]; + let gatewayServer: ApolloServer; + let gatewayUrl: string; + let reportPromise: Promise; + let nockScope: nock.Scope; + + beforeEach(async () => { + let reportResolver: (report: any) => void; + reportPromise = new Promise((resolve) => { + reportResolver = resolve; + }); + + nockScope = nock('https://engine-report.apollodata.com') + .post('/api/ingress/traces') + .reply(200, (_: any, requestBody: string) => { + reportResolver(requestBody); + return 'ok'; + }); + + backendServers = []; + const serviceList = []; + for (const fixture of fixtures) { + const { server, url } = await startFederatedServer([fixture]); + backendServers.push(server); + serviceList.push({ name: fixture.name, url }); + } + + const gateway = new ApolloGateway({ serviceList }); + const { schema, executor } = await gateway.load(); + gatewayServer = new ApolloServer({ + schema, + executor, + engine: { + apiKey: 'service:foo:bar', + sendReportsImmediately: true, + }, + }); + ({ url: gatewayUrl } = await gatewayServer.listen({ port: 0 })); + }); + + afterEach(async () => { + for (const server of backendServers) { + await server.stop(); + } + if (gatewayServer) { + await gatewayServer.stop(); + } + nockScope.done(); + }); + + it(`queries three services`, async () => { + const query = gql` + query { + me { + name { + first + last + } + } + topProducts { + name + } + } + `; + + const result = await toPromise( + execute(createHttpLink({ uri: gatewayUrl, fetch: fetch as any }), { + query, + }), + ); + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "me": Object { + "name": Object { + "first": "Ada", + "last": "Lovelace", + }, + }, + "topProducts": Array [ + Object { + "name": "Table", + }, + Object { + "name": "Couch", + }, + Object { + "name": "Chair", + }, + Object { + "name": "Structure and Interpretation of Computer Programs (1996)", + }, + Object { + "name": "Object Oriented Software Construction (1997)", + }, + ], + }, + } + `); + const reportBody = await reportPromise; + // nock returns binary bodies as hex strings + const gzipReportBuffer = Buffer.from(reportBody, 'hex'); + const reportBuffer = gunzipSync(gzipReportBuffer); + const report = Report.decode(reportBuffer); + + // Some handwritten tests to capture salient properties. + const statsReportKey = '# -\n{me{name{first last}}topProducts{name}}'; + expect(Object.keys(report.tracesPerQuery)).toStrictEqual([statsReportKey]); + expect(report.tracesPerQuery[statsReportKey]!.trace!.length).toBe(1); + const trace = report.tracesPerQuery[statsReportKey]!.trace![0]!; + // In the gateway, the root trace is just an empty node (unless there are errors). + expect(trace.root!.child).toStrictEqual([]); + // The query plan has (among other things) a fetch against 'accounts' and a + // fetch against 'product'. + expect(trace.queryPlan).toBeTruthy(); + const queryPlan = trace.queryPlan!; + expect(queryPlan.parallel).toBeTruthy(); + expect(queryPlan.parallel!.nodes![0]!.fetch!.serviceName).toBe('accounts'); + expect( + queryPlan.parallel!.nodes![0]!.fetch!.trace!.root!.child![0]! + .responseName, + ).toBe('me'); + expect(queryPlan.parallel!.nodes![1]!.sequence).toBeTruthy(); + expect( + queryPlan.parallel!.nodes![1]!.sequence!.nodes![0]!.fetch!.serviceName, + ).toBe('product'); + expect( + queryPlan.parallel!.nodes![1]!.sequence!.nodes![0]!.fetch!.trace!.root! + .child![0].responseName, + ).toBe('topProducts'); + + expect(report).toMatchInlineSnapshot(` + Object { + "endTime": null, + "header": "
", + "tracesPerQuery": Object { + "# - + {me{name{first last}}topProducts{name}}": Object { + "trace": Array [ + Object { + "clientName": "", + "clientReferenceId": "", + "clientVersion": "", + "details": Object {}, + "durationNs": 12345, + "endTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "forbiddenOperation": false, + "fullQueryCacheHit": false, + "http": Object { + "method": "POST", + }, + "queryPlan": Object { + "parallel": Object { + "nodes": Array [ + Object { + "fetch": Object { + "receivedTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTimeOffset": 23456, + "serviceName": "accounts", + "trace": Object { + "durationNs": 12345, + "endTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "root": Object { + "child": Array [ + Object { + "child": Array [ + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Name", + "responseName": "first", + "startTime": "34567", + "type": "String", + }, + Object { + "endTime": "45678", + "parentType": "Name", + "responseName": "last", + "startTime": "34567", + "type": "String", + }, + ], + "endTime": "45678", + "parentType": "User", + "responseName": "name", + "startTime": "34567", + "type": "Name", + }, + ], + "endTime": "45678", + "parentType": "Query", + "responseName": "me", + "startTime": "34567", + "type": "User", + }, + ], + }, + "startTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + }, + "traceParsingFailed": false, + }, + }, + Object { + "sequence": Object { + "nodes": Array [ + Object { + "fetch": Object { + "receivedTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTimeOffset": 23456, + "serviceName": "product", + "trace": Object { + "durationNs": 12345, + "endTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "root": Object { + "child": Array [ + Object { + "child": Array [ + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Furniture", + "responseName": "name", + "startTime": "34567", + "type": "String", + }, + ], + "index": 0, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Furniture", + "responseName": "name", + "startTime": "34567", + "type": "String", + }, + ], + "index": 1, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Furniture", + "responseName": "name", + "startTime": "34567", + "type": "String", + }, + ], + "index": 2, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "isbn", + "startTime": "34567", + "type": "String!", + }, + ], + "index": 3, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "isbn", + "startTime": "34567", + "type": "String!", + }, + ], + "index": 4, + }, + ], + "endTime": "45678", + "parentType": "Query", + "responseName": "topProducts", + "startTime": "34567", + "type": "[Product]", + }, + ], + }, + "startTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + }, + "traceParsingFailed": false, + }, + }, + Object { + "flatten": Object { + "node": Object { + "fetch": Object { + "receivedTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTimeOffset": 23456, + "serviceName": "books", + "trace": Object { + "durationNs": 12345, + "endTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "root": Object { + "child": Array [ + Object { + "child": Array [ + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "isbn", + "startTime": "34567", + "type": "String!", + }, + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "title", + "startTime": "34567", + "type": "String", + }, + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "year", + "startTime": "34567", + "type": "Int", + }, + ], + "index": 0, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "isbn", + "startTime": "34567", + "type": "String!", + }, + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "title", + "startTime": "34567", + "type": "String", + }, + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "year", + "startTime": "34567", + "type": "Int", + }, + ], + "index": 1, + }, + ], + "endTime": "45678", + "parentType": "Query", + "responseName": "_entities", + "startTime": "34567", + "type": "[_Entity]!", + }, + ], + }, + "startTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + }, + "traceParsingFailed": false, + }, + }, + "responsePath": Array [ + Object { + "fieldName": "topProducts", + }, + Object { + "fieldName": "@", + }, + ], + }, + }, + Object { + "flatten": Object { + "node": Object { + "fetch": Object { + "receivedTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "sentTimeOffset": 23456, + "serviceName": "product", + "trace": Object { + "durationNs": 12345, + "endTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + "root": Object { + "child": Array [ + Object { + "child": Array [ + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "name", + "startTime": "34567", + "type": "String", + }, + ], + "index": 0, + }, + Object { + "child": Array [ + Object { + "endTime": "45678", + "parentType": "Book", + "responseName": "name", + "startTime": "34567", + "type": "String", + }, + ], + "index": 1, + }, + ], + "endTime": "45678", + "parentType": "Query", + "responseName": "_entities", + "startTime": "34567", + "type": "[_Entity]!", + }, + ], + }, + "startTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + }, + "traceParsingFailed": false, + }, + }, + "responsePath": Array [ + Object { + "fieldName": "topProducts", + }, + Object { + "fieldName": "@", + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + "registeredOperation": false, + "root": Object {}, + "startTime": Object { + "nanos": 123000000, + "seconds": "1562203363", + }, + }, + ], + }, + }, + } + `); + }); +}); diff --git a/gateway-js/src/__tests__/integration/abstract-types.test.ts b/gateway-js/src/__tests__/integration/abstract-types.test.ts new file mode 100644 index 000000000..2bad0c271 --- /dev/null +++ b/gateway-js/src/__tests__/integration/abstract-types.test.ts @@ -0,0 +1,830 @@ +import gql from 'graphql-tag'; +import { execute } from '../execution-utils'; + +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +it('handles an abstract type from the base service', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + upc + name + price + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ + product: { + upc, + name: 'Table', + price: '899', + }, + }); + + expect(queryPlan).toCallService('product'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + product(upc: $upc) { + __typename + ... on Book { + upc + __typename + isbn + price + } + ... on Furniture { + upc + name + price + } + } + } + }, + Flatten(path: "product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + } + `); +}); + +it('can request fields on extended interfaces', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + } + } + `; + + const upc = '1'; + + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ product: { inStock: true } }); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('inventory'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + product(upc: $upc) { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } + } + }, + Flatten(path: "product") { + Fetch(service: "inventory") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } => + { + ... on Book { + inStock + } + ... on Furniture { + inStock + } + } + }, + }, + }, + } + `); +}); + +it('can request fields on extended types that implement an interface', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + ... on Furniture { + isHeavy + } + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ product: { inStock: true, isHeavy: false } }); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('inventory'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + product(upc: $upc) { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } + } + }, + Flatten(path: "product") { + Fetch(service: "inventory") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } => + { + ... on Book { + inStock + } + ... on Furniture { + inStock + isHeavy + } + } + }, + }, + }, + } + `); +}); + +it('prunes unfilled type conditions', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + inStock + ... on Furniture { + isHeavy + } + ... on Book { + isCheckedOut + } + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ product: { inStock: true, isHeavy: false } }); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('inventory'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + product(upc: $upc) { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } + } + }, + Flatten(path: "product") { + Fetch(service: "inventory") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + sku + } + } => + { + ... on Book { + inStock + isCheckedOut + } + ... on Furniture { + inStock + isHeavy + } + } + }, + }, + }, + } + `); +}); + +it('fetches interfaces returned from other services', async () => { + const query = `#graphql + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Book { + title + } + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + reviews: [ + { product: { price: '899' } }, + { product: { price: '1299' } }, + { product: { price: '49', title: 'Design Patterns' } }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + } + } + } + }, + }, + Parallel { + Flatten(path: "me.reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + price + } + ... on Furniture { + price + } + } + }, + }, + Flatten(path: "me.reviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + title + } + } + }, + }, + }, + }, + } + `); +}); + +it('fetches composite fields from a foreign type casted to an interface [@provides field]', async () => { + const query = `#graphql + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Book { + name + } + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + reviews: [ + { product: { price: '899' } }, + { product: { price: '1299' } }, + { product: { price: '49', name: 'Design Patterns (1995)' } }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + } + } + } + }, + }, + Parallel { + Flatten(path: "me.reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + price + } + ... on Furniture { + price + } + } + }, + }, + Sequence { + Flatten(path: "me.reviews.@.product") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + __typename + isbn + title + year + } + } + }, + }, + Flatten(path: "me.reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + title + year + } + } => + { + ... on Book { + name + } + } + }, + }, + }, + }, + }, + } + `); +}); + +it('allows for extending an interface from another service with fields', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + reviews { + body + } + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ + product: { + reviews: [{ body: 'Love it!' }, { body: 'Prefer something else.' }], + }, + }); + + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + product(upc: $upc) { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + } + }, + Flatten(path: "product") { + Fetch(service: "reviews") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + reviews { + body + } + } + ... on Furniture { + reviews { + body + } + } + } + }, + }, + }, + } + `); +}); + +describe('unions', () => { + it('handles unions from the same service', async () => { + const query = `#graphql + query GetUserAndProducts { + me { + reviews { + product { + price + ... on Furniture { + brand { + ... on Ikea { + asile + } + ... on Amazon { + referrer + } + } + } + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + reviews: [ + { product: { price: '899', brand: { asile: 10 } } }, + { + product: { + price: '1299', + brand: { referrer: 'https://canopy.co' }, + }, + }, + { product: { price: '49' } }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "accounts") { + { + me { + __typename + id + } + } + }, + Flatten(path: "me") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } + } + } + } + }, + }, + Flatten(path: "me.reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + price + } + ... on Furniture { + price + brand { + __typename + ... on Ikea { + asile + } + ... on Amazon { + referrer + } + } + } + } + }, + }, + }, + } + `); + }); + + it("doesn't expand interfaces with inline type conditions if all possibilities are fufilled by one service", async () => { + const query = `#graphql + query GetProducts { + topProducts { + name + } + } + `; + + const { queryPlan, errors } = await execute({ query }, [ + { + name: 'products', + typeDefs: gql` + extend type Query { + topProducts: [Product] + } + + interface Product { + name: String + } + + type Shoe implements Product { + name: String + } + + type Car implements Product { + name: String + } + `, + }, + ]); + + expect(errors).toBeUndefined(); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "products") { + { + topProducts { + __typename + name + } + } + }, + } + `); + }); + + // FIXME: turn back on when extending unions is supported in composition + it.todo('fetches unions across services'); + // async () => { + // const query = gql` + // query GetUserAndProducts { + // me { + // account { + // ... on LibraryAccount { + // library { + // name + // } + // } + // ... on SMSAccount { + // number + // } + // } + // } + // } + // `; + + // const { data, queryPlan } = await execute( + // { + // query, + // }, + // ); + + // expect(data).toEqual({ + // me: { + // account: { + // library: { + // name: 'NYC Public Library', + // }, + // }, + // }, + // }); + + // expect(queryPlan).toCallService('accounts'); + // expect(queryPlan).toCallService('books'); + // }); +}); diff --git a/gateway-js/src/__tests__/integration/aliases.test.ts b/gateway-js/src/__tests__/integration/aliases.test.ts new file mode 100644 index 000000000..e86d7d2ec --- /dev/null +++ b/gateway-js/src/__tests__/integration/aliases.test.ts @@ -0,0 +1,176 @@ +import { execute } from '../execution-utils'; +// FIXME: remove this when GraphQLExtensions is removed +import { createTestClient } from 'apollo-server-testing'; +import { ApolloServerBase as ApolloServer } from 'apollo-server-core'; +import { buildFederatedSchema } from '@apollo/federation'; +import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource'; +import { ApolloGateway } from '../../'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +it('supports simple aliases', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ + product: { + name: 'Table', + title: 'Table', + }, + }); + + expect(queryPlan).toCallService('product'); +}); + +it('supports aliases of root fields on subservices', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + reviews { + body + } + productReviews: reviews { + body + } + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ + product: { + name: 'Table', + title: 'Table', + reviews: [ + { + body: 'Love it!', + }, + { + body: 'Prefer something else.', + }, + ], + productReviews: [ + { + body: 'Love it!', + }, + { + body: 'Prefer something else.', + }, + ], + }, + }); + + expect(queryPlan).toCallService('product'); +}); + +it('supports aliases of nested fields on subservices', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + name + title: name + reviews { + content: body + body + } + productReviews: reviews { + body + reviewer: author { + name: username + } + } + } + } + `; + + const upc = '1'; + const { data, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(data).toEqual({ + product: { + name: 'Table', + title: 'Table', + reviews: [ + { + content: 'Love it!', + body: 'Love it!', + }, + { + content: 'Prefer something else.', + body: 'Prefer something else.', + }, + ], + productReviews: [ + { + body: 'Love it!', + reviewer: { + name: '@ada', + }, + }, + { + body: 'Prefer something else.', + reviewer: { + name: '@complete', + }, + }, + ], + }, + }); + + expect(queryPlan).toCallService('product'); +}); + +// TODO after we remove GraphQLExtensions from ApolloServer, this can go away +it('supports aliases when using ApolloServer', async () => { + const gateway = new ApolloGateway({ + localServiceList: fixtures, + buildService: service => { + return new LocalGraphQLDataSource(buildFederatedSchema([service])); + }, + }); + + const { schema, executor } = await gateway.load(); + + const server = new ApolloServer({ schema, executor }); + + const upc = '1'; + const { query } = createTestClient(server); + + const result = await query({ + query: `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + title: name + } + } + `, + variables: { upc }, + }); + + expect(result.data).toEqual({ + product: { + title: 'Table', + }, + }); +}); diff --git a/gateway-js/src/__tests__/integration/boolean.test.ts b/gateway-js/src/__tests__/integration/boolean.test.ts new file mode 100644 index 000000000..2e50eb837 --- /dev/null +++ b/gateway-js/src/__tests__/integration/boolean.test.ts @@ -0,0 +1,277 @@ +import { execute } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +// TODO: right now the query planner doesn't prune known skip and include points +// eventually we want to do this to prevent downstream fetches that aren't needed +describe('@skip', () => { + it('supports @skip when a boolean condition is met', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body + author @skip(if: true) { + name + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + it('supports @skip when a boolean condition is met (variable driven)', async () => { + const query = `#graphql + query GetReviewers($skip: Boolean!) { + topReviews { + body + author @skip(if: $skip) { + username + } + } + } + `; + + const skip = true; + const { data, queryPlan } = await execute({ + query, + variables: { skip }, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); + + expect(queryPlan).not.toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + // Data looks good here, suspect the matcher is incorrect + it('supports @skip when a boolean condition is not met', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body + author @skip(if: false) { + name { + first + last + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + // Data looks good here, suspect the matcher is incorrect + it('supports @skip when a boolean condition is not met (variable driven)', async () => { + const query = `#graphql + query GetReviewers($skip: Boolean!) { + topReviews { + body + author @skip(if: $skip) { + name { + first + last + } + } + } + } + `; + + const skip = false; + const { data, queryPlan } = await execute({ + query, + variables: { skip }, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); +}); + +describe('@include', () => { + it('supports @include when a boolean condition is not met', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body + author @include(if: false) { + username + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); + + expect(queryPlan).not.toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + it('supports @include when a boolean condition is not met (variable driven)', async () => { + const query = `#graphql + query GetReviewers($include: Boolean!) { + topReviews { + body + author @include(if: $include) { + username + } + } + } + `; + + const include = false; + const { data, queryPlan } = await execute({ + query, + variables: { include }, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); + + expect(queryPlan).not.toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + // Data looks good here, suspect the matcher is incorrect + // Added the query plan snapshot for a view. + it('supports @include when a boolean condition is met', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body + author @include(if: true) { + name { + first + last + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); + + // Data looks good here, suspect the matcher is incorrect + // Added the query plan snapshot for a view. + it('supports @include when a boolean condition is met (variable driven)', async () => { + const query = `#graphql + query GetReviewers($include: Boolean!) { + topReviews { + body + author @include(if: $include) { + name { + first + last + } + } + } + } + `; + + const include = true; + const { data, queryPlan } = await execute({ + query, + variables: { include }, + }); + + expect(data).toEqual({ + topReviews: [ + { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, + { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + }); +}); diff --git a/gateway-js/src/__tests__/integration/complex-key.test.ts b/gateway-js/src/__tests__/integration/complex-key.test.ts new file mode 100644 index 000000000..a413c3a7a --- /dev/null +++ b/gateway-js/src/__tests__/integration/complex-key.test.ts @@ -0,0 +1,217 @@ +import gql from 'graphql-tag'; +import { execute, ServiceDefinitionModule } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +const users = [ + { id: '1', name: 'Trevor Scheer', organizationId: '1', __typename: 'User' }, + { id: '1', name: 'Trevor Scheer', organizationId: '2', __typename: 'User' }, + { id: '2', name: 'James Baxley', organizationId: '1', __typename: 'User' }, + { id: '2', name: 'James Baxley', organizationId: '3', __typename: 'User' }, +]; + +const organizations = [ + { id: '1', name: 'Apollo', __typename: 'Organization' }, + { id: '2', name: 'Wayfair', __typename: 'Organization' }, + { id: '3', name: 'Major League Soccer', __typename: 'Organization' }, +]; + +const reviews = [ + { id: '1', authorId: '1', organizationId: '1', __typename: 'Review' }, + { id: '2', authorId: '1', organizationId: '2', __typename: 'Review' }, + { id: '3', authorId: '2', organizationId: '1', __typename: 'Review' }, + { id: '4', authorId: '2', organizationId: '3', __typename: 'Review' }, +]; + +const reviewService: ServiceDefinitionModule = { + name: 'review', + typeDefs: gql` + type Query { + reviews: [Review!]! + } + + type Review { + id: ID! + author: User! + body: String! + } + + # TODO: consider ergonomics of external types. + extend type User @key(fields: "id organization { id }") { + id: ID! @external + organization: Organization! @external + } + + extend type Organization { + id: ID! @external + } + `, + resolvers: { + Query: { + reviews() { + return reviews; + }, + }, + Review: { + author(review) { + return { + id: review.authorId, + organization: { + id: review.organizationId, + }, + }; + }, + }, + }, +}; + +const userService: ServiceDefinitionModule = { + name: 'user', + typeDefs: gql` + type User @key(fields: "id organization { id }") { + id: ID! + name: String! + organization: Organization! + } + + type Organization @key(fields: "id") { + id: ID! + name: String! + } + `, + resolvers: { + User: { + __resolveReference(reference) { + return users.find( + user => + user.id === reference.id && + user.organizationId === reference.organization.id, + ); + }, + organization(user) { + return { id: user.organizationId }; + }, + }, + Organization: { + __resolveObject(object) { + return organizations.find(org => org.id === object.id); + }, + }, + }, +}; + +it('works fetches data correctly with complex / nested @key fields', async () => { + const query = `#graphql + query Reviews { + reviews { + author { + name + organization { + name + } + } + } + } + `; + + const { data, queryPlan } = await execute( + { + query, + }, + [userService, reviewService], + ); + + expect(data).toEqual({ + reviews: [ + { + author: { + name: 'Trevor Scheer', + organization: { + name: 'Apollo', + }, + }, + }, + { + author: { + name: 'Trevor Scheer', + organization: { + name: 'Wayfair', + }, + }, + }, + { + author: { + name: 'James Baxley', + organization: { + name: 'Apollo', + }, + }, + }, + { + author: { + name: 'James Baxley', + organization: { + name: 'Major League Soccer', + }, + }, + }, + ], + }); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "review") { + { + reviews { + author { + __typename + id + organization { + id + __typename + } + } + } + } + }, + Parallel { + Flatten(path: "reviews.@.author") { + Fetch(service: "user") { + { + ... on User { + __typename + id + organization { + id + } + } + } => + { + ... on User { + name + } + } + }, + }, + Flatten(path: "reviews.@.author.organization") { + Fetch(service: "user") { + { + ... on Organization { + __typename + id + } + } => + { + ... on Organization { + name + } + } + }, + }, + }, + }, + } + `); +}); diff --git a/gateway-js/src/__tests__/integration/custom-directives.test.ts b/gateway-js/src/__tests__/integration/custom-directives.test.ts new file mode 100644 index 000000000..5b490fbb7 --- /dev/null +++ b/gateway-js/src/__tests__/integration/custom-directives.test.ts @@ -0,0 +1,165 @@ +import gql from 'graphql-tag'; +import { execute } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +describe('custom executable directives', () => { + it('successfully passes directives along in requests to an underlying service', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body @stream + } + } + `; + + const { errors, queryPlan } = await execute({ + query, + }); + + expect(errors).toBeUndefined(); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "reviews") { + { + topReviews { + body @stream + } + } + }, + } + `); + }); + + it('successfully passes directives and their variables along in requests to underlying services', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + body @stream + author @transform(from: "JSON") { + name @stream { + first + last + } + } + } + } + `; + + const { errors, queryPlan } = await execute({ + query, + }); + + expect(errors).toBeUndefined(); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + topReviews { + body @stream + author @transform(from: "JSON") { + __typename + id + } + } + } + }, + Flatten(path: "topReviews.@.author") { + Fetch(service: "accounts") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name @stream { + first + last + } + } + } + }, + }, + }, + } + `); + }); + + it("returns validation errors when directives aren't present across all services", async () => { + const invalidService = { + name: 'invalidService', + typeDefs: gql` + directive @invalid on QUERY + `, + }; + + const query = `#graphql + query GetReviewers { + topReviews { + body @stream + } + } + `; + + expect( + execute( + { + query, + }, + [...fixtures, invalidService], + ), + ).rejects.toThrowErrorMatchingInlineSnapshot(` +"[@stream] -> Custom directives must be implemented in every service. The following services do not implement the @stream directive: invalidService. + +[@transform] -> Custom directives must be implemented in every service. The following services do not implement the @transform directive: invalidService. + +[@invalid] -> Custom directives must be implemented in every service. The following services do not implement the @invalid directive: accounts, books, documents, inventory, product, reviews." +`); + }); + + it("returns validation errors when directives aren't identical across all services", async () => { + const invalidService = { + name: 'invalid', + typeDefs: gql` + directive @stream on QUERY + `, + }; + + const query = `#graphql + query GetReviewers { + topReviews { + body @stream + } + } + `; + + expect( + execute( + { + query, + }, + [...fixtures, invalidService], + ), + ).rejects.toThrowErrorMatchingInlineSnapshot(` +"[@transform] -> Custom directives must be implemented in every service. The following services do not implement the @transform directive: invalid. + +[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: + accounts: directive @stream on FIELD + books: directive @stream on FIELD + documents: directive @stream on FIELD + inventory: directive @stream on FIELD + product: directive @stream on FIELD + reviews: directive @stream on FIELD + invalid: directive @stream on QUERY" +`); + }); +}); diff --git a/gateway-js/src/__tests__/integration/execution-style.test.ts b/gateway-js/src/__tests__/integration/execution-style.test.ts new file mode 100644 index 000000000..30a1088ac --- /dev/null +++ b/gateway-js/src/__tests__/integration/execution-style.test.ts @@ -0,0 +1,35 @@ +import { execute } from '../execution-utils'; + +describe('query', () => { + it('supports parallel root fields', async () => { + const query = `#graphql + query GetUserAndReviews { + me { + username + } + topReviews { + body + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { username: '@ada' }, + topReviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'Could be better.' }, + { body: 'Prefer something else.' }, + { body: 'Wish I had read this before.' }, + ], + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + // FIXME: determine matcher for execution order + }); +}); diff --git a/gateway-js/src/__tests__/integration/fragments.test.ts b/gateway-js/src/__tests__/integration/fragments.test.ts new file mode 100644 index 000000000..939cac59c --- /dev/null +++ b/gateway-js/src/__tests__/integration/fragments.test.ts @@ -0,0 +1,237 @@ +import { disableFragmentWarnings } from 'graphql-tag'; +import { execute } from '../execution-utils'; + +beforeAll(() => { + disableFragmentWarnings(); +}); +it('supports inline fragments (one level)', async () => { + const query = `#graphql + query GetUser { + me { + ... on User { + username + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + }, + }); + + expect(queryPlan).toCallService('accounts'); +}); + +it('supports inline fragments (multi level)', async () => { + const query = `#graphql + query GetUser { + me { + ... on User { + username + reviews { + ... on Review { + body + product { + ... on Product { + ... on Book { + title + } + ... on Furniture { + name + } + } + } + } + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + reviews: [ + { body: 'Love it!', product: { name: 'Table' } }, + { body: 'Too expensive.', product: { name: 'Couch' } }, + { body: 'A classic.', product: { title: 'Design Patterns' } }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('books'); +}); + +it('supports named fragments (one level)', async () => { + const query = `#graphql + query GetUser { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + }, + }); + + expect(queryPlan).toCallService('accounts'); +}); + +it('supports multiple named fragments (one level, mixed ordering)', async () => { + const query = `#graphql + fragment userInfo on User { + name { + first + last + } + } + query GetUser { + me { + ...userDetails + ...userInfo + } + } + + fragment userDetails on User { + username + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + name: { + first: 'Ada', + last: 'Lovelace', + } + }, + }); + + expect(queryPlan).toCallService('accounts'); +}); + +it('supports multiple named fragments (multi level, mixed ordering)', async () => { + const query = `#graphql + fragment reviewDetails on Review { + body + } + query GetUser { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + reviews { + ...reviewDetails + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + reviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'A classic.' }, + ], + username: '@ada', + }, + }); + + expect(queryPlan).toCallService('accounts'); +}); + +it('supports variables within fragments', async () => { + const query = `#graphql + query GetUser($format: Boolean) { + me { + ...userDetails + } + } + + fragment userDetails on User { + username + reviews { + body(format: $format) + } + } + `; + + const format = true; + const { data, queryPlan } = await execute({ + query, + variables: { format }, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + reviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { body: 'A classic.' }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); +}); + +it('supports root fragments', async () => { + const query = `#graphql + query GetUser { + ... on Query { + me { + username + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + username: '@ada', + }, + }); + + expect(queryPlan).toCallService('accounts'); +}); diff --git a/gateway-js/src/__tests__/integration/list-key.test.ts b/gateway-js/src/__tests__/integration/list-key.test.ts new file mode 100644 index 000000000..1573ecd99 --- /dev/null +++ b/gateway-js/src/__tests__/integration/list-key.test.ts @@ -0,0 +1,128 @@ +import gql from 'graphql-tag'; +import { execute, ServiceDefinitionModule } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +const users = [ + { id: ['1', '1'], name: 'Trevor Scheer', __typename: 'User' }, + { id: ['2', '2'], name: 'James Baxley', __typename: 'User' }, +]; + +const reviews = [ + { id: '1', authorId: ['1', '1'], body: 'Good', __typename: 'Review' }, + { id: '2', authorId: ['2', '2'], body: 'Bad', __typename: 'Review' }, +]; + +const reviewService: ServiceDefinitionModule = { + name: 'review', + typeDefs: gql` + type Query { + reviews: [Review!]! + } + + type Review { + id: ID! + author: User! + body: String! + } + + extend type User @key(fields: "id") { + id: [ID!]! @external + } + `, + resolvers: { + Query: { + reviews() { + return reviews; + }, + }, + Review: { + author(review) { + return { + id: review.authorId, + }; + }, + }, + }, +}; + +const listsAreEqual = (as: T[], bs: T[]) => + as.length === bs.length && as.every((a, i) => bs[i] === as[i]); + +const userService: ServiceDefinitionModule = { + name: 'user', + typeDefs: gql` + type User @key(fields: "id") { + id: [ID!]! + name: String! + } + `, + resolvers: { + User: { + __resolveReference(reference) { + return users.find(user => listsAreEqual(user.id, reference.id)); + }, + }, + }, +}; + +it('fetches data correctly list type @key fields', async () => { + const query = `#graphql + query Reviews { + reviews { + body + author { + name + } + } + } + `; + + const { data, queryPlan } = await execute( + { + query, + }, + [userService, reviewService], + ); + + expect(data).toEqual({ + reviews: [ + { body: 'Good', author: { name: 'Trevor Scheer' } }, + { body: 'Bad', author: { name: 'James Baxley' } }, + ], + }); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "review") { + { + reviews { + body + author { + __typename + id + } + } + } + }, + Flatten(path: "reviews.@.author") { + Fetch(service: "user") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + } + } + }, + }, + }, + } + `); +}); diff --git a/gateway-js/src/__tests__/integration/logger.test.ts b/gateway-js/src/__tests__/integration/logger.test.ts new file mode 100644 index 000000000..9efd876a6 --- /dev/null +++ b/gateway-js/src/__tests__/integration/logger.test.ts @@ -0,0 +1,122 @@ +import { ApolloGateway } from '../..'; +import { Logger } from "apollo-server-types"; +import { PassThrough } from "stream"; + +import * as winston from "winston"; +import WinstonTransport from 'winston-transport'; +import * as bunyan from "bunyan"; +import * as loglevel from "loglevel"; +import * as log4js from "log4js"; + +const LOWEST_LOG_LEVEL = "debug"; + +const KNOWN_DEBUG_MESSAGE = "Checking service definitions..."; + +async function triggerKnownDebugMessage(logger: Logger) { + // Trigger a known error. + // This is a bit brittle since it merely leverages a known debug log + // message outside of the constructor, but it seemed worth testing + // the compatibility with `ApolloGateway` itself rather than generically. + // The error does not matter, so it is caught and ignored. + await new ApolloGateway({ logger }).load().catch(_e => {}); +} + +describe("logger", () => { + it("works with 'winston'", async () => { + const sink = jest.fn(); + const transport = new class extends WinstonTransport { + constructor() { + super({ + format: winston.format.json(), + }); + } + + log(info: any) { + sink(info); + } + }; + + const logger = winston.createLogger({ level: 'debug' }).add(transport); + + await triggerKnownDebugMessage(logger); + + expect(sink).toHaveBeenCalledWith(expect.objectContaining({ + level: LOWEST_LOG_LEVEL, + message: KNOWN_DEBUG_MESSAGE, + })); + }); + + it("works with 'bunyan'", async () => { + const sink = jest.fn(); + + // Bunyan uses streams for its logging implementations. + const writable = new PassThrough(); + writable.on("data", data => sink(JSON.parse(data.toString()))); + + const logger = bunyan.createLogger({ + name: "test-logger-bunyan", + streams: [{ + level: LOWEST_LOG_LEVEL, + stream: writable, + }] + }); + + await triggerKnownDebugMessage(logger); + + expect(sink).toHaveBeenCalledWith(expect.objectContaining({ + level: bunyan.DEBUG, + msg: KNOWN_DEBUG_MESSAGE, + })); + }); + + it("works with 'loglevel'", async () => { + const sink = jest.fn(); + + const logger = loglevel.getLogger("test-logger-loglevel") + logger.methodFactory = (_methodName, level): loglevel.LoggingMethod => + (message) => sink({ level, message }); + + // The `setLevel` method must be called after overwriting `methodFactory`. + // This is an intentional API design pattern of the loglevel package: + // https://www.npmjs.com/package/loglevel#writing-plugins + logger.setLevel(loglevel.levels.DEBUG); + + await triggerKnownDebugMessage(logger); + + expect(sink).toHaveBeenCalledWith({ + level: loglevel.levels.DEBUG, + message: KNOWN_DEBUG_MESSAGE, + }); + }); + + it("works with 'log4js'", async () => { + const sink = jest.fn(); + + log4js.configure({ + appenders: { + custom: { + type: { + configure: () => + (loggingEvent: log4js.LoggingEvent) => sink(loggingEvent) + } + } + }, + categories: { + default: { + appenders: ['custom'], + level: LOWEST_LOG_LEVEL, + } + } + }); + + const logger = log4js.getLogger(); + logger.level = LOWEST_LOG_LEVEL; + + await triggerKnownDebugMessage(logger); + + expect(sink).toHaveBeenCalledWith(expect.objectContaining({ + level: log4js.levels.DEBUG, + data: [KNOWN_DEBUG_MESSAGE], + })); + }); +}); diff --git a/gateway-js/src/__tests__/integration/merge-arrays.test.ts b/gateway-js/src/__tests__/integration/merge-arrays.test.ts new file mode 100644 index 000000000..2748b0d57 --- /dev/null +++ b/gateway-js/src/__tests__/integration/merge-arrays.test.ts @@ -0,0 +1,34 @@ +import { execute } from '../execution-utils'; + +describe('query', () => { + it('supports arrays', async () => { + const query = `#graphql + query MergeArrays { + me { + # goodAddress + goodDescription + metadata { + address + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + goodDescription: true, + metadata: [ + { + address: '1', + }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + }); +}); diff --git a/gateway-js/src/__tests__/integration/multiple-key.test.ts b/gateway-js/src/__tests__/integration/multiple-key.test.ts new file mode 100644 index 000000000..60450252d --- /dev/null +++ b/gateway-js/src/__tests__/integration/multiple-key.test.ts @@ -0,0 +1,328 @@ +import gql from 'graphql-tag'; +import { execute, ServiceDefinitionModule } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +const users = [ + { ssn: '111-11-1111', name: 'Trevor', id: '10', __typename: 'User' }, + { ssn: '222-22-2222', name: 'Scheer', id: '20', __typename: 'User' }, + { ssn: '333-33-3333', name: 'James', id: '30', __typename: 'User' }, + { ssn: '444-44-4444', name: 'Baxley', id: '40', __typename: 'User' }, +]; + +const reviews = [ + { id: '1', authorId: '10', body: 'A', __typename: 'Review' }, + { id: '2', authorId: '20', body: 'B', __typename: 'Review' }, + { id: '3', authorId: '30', body: 'C', __typename: 'Review' }, + { id: '4', authorId: '40', body: 'D', __typename: 'Review' }, +]; + +const reviewService: ServiceDefinitionModule = { + name: 'reviews', + typeDefs: gql` + extend type Query { + reviews: [Review!]! + } + + type Review { + id: ID! + author: User! + body: String! + } + + extend type User @key(fields: "id") { + id: ID! @external + reviews: [Review!]! + } + `, + resolvers: { + Query: { + reviews() { + return reviews; + }, + }, + User: { + reviews(user) { + return reviews.filter(review => review.authorId === user.id); + }, + }, + Review: { + author(review) { + return { + id: review.authorId, + }; + }, + }, + }, +}; + +const actuaryService: ServiceDefinitionModule = { + name: 'actuary', + typeDefs: gql` + extend type User @key(fields: "ssn") { + ssn: ID! @external + risk: Float + } + `, + resolvers: { + User: { + risk(user) { + return user.ssn[0] / 10; + }, + }, + }, +}; + +const userService: ServiceDefinitionModule = { + name: 'users', + typeDefs: gql` + extend type Query { + users: [User!]! + } + + type Group { + id: ID + name: String + } + + type User + @key(fields: "ssn") + @key(fields: "id") + @key(fields: "group { id }") { + id: ID! + ssn: ID! + name: String! + group: Group + } + `, + resolvers: { + Query: { + users() { + return users; + }, + }, + User: { + group: () => ({ id: 1, name: 'Apollo GraphQL' }), + __resolveReference(reference) { + if (reference.ssn) + return users.find(user => user.ssn === reference.ssn); + else return users.find(user => user.id === reference.id); + }, + }, + }, +}; + +it('fetches data correctly with multiple @key fields', async () => { + const query = `#graphql + query { + reviews { + body + author { + name + risk + } + } + } + `; + + const { data, queryPlan, errors } = await execute( + { + query, + }, + [userService, reviewService, actuaryService], + ); + + expect(errors).toBeFalsy(); + expect(data).toEqual({ + reviews: [ + { + body: 'A', + author: { + risk: 0.1, + name: 'Trevor', + }, + }, + { + body: 'B', + author: { + risk: 0.2, + name: 'Scheer', + }, + }, + { + body: 'C', + author: { + risk: 0.3, + name: 'James', + }, + }, + { + body: 'D', + author: { + risk: 0.4, + name: 'Baxley', + }, + }, + ], + }); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + reviews { + body + author { + __typename + id + } + } + } + }, + Flatten(path: "reviews.@.author") { + Fetch(service: "users") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + name + __typename + ssn + } + } + }, + }, + Flatten(path: "reviews.@.author") { + Fetch(service: "actuary") { + { + ... on User { + __typename + ssn + } + } => + { + ... on User { + risk + } + } + }, + }, + }, + } + `); +}); + +it('fetches keys as needed to reduce round trip queries', async () => { + const query = `#graphql + query { + users { + risk + reviews { + body + } + } + } + `; + + const { data, queryPlan, errors } = await execute( + { + query, + }, + [userService, reviewService, actuaryService] + ); + + expect(errors).toBeFalsy(); + expect(data).toEqual({ + users: [ + { + risk: 0.1, + reviews: [ + { + body: 'A', + }, + ], + }, + { + risk: 0.2, + reviews: [ + { + body: 'B', + }, + ], + }, + { + risk: 0.3, + reviews: [ + { + body: 'C', + }, + ], + }, + { + risk: 0.4, + reviews: [ + { + body: 'D', + }, + ], + }, + ], + }); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "users") { + { + users { + __typename + ssn + id + } + } + }, + Parallel { + Flatten(path: "users.@") { + Fetch(service: "actuary") { + { + ... on User { + __typename + ssn + } + } => + { + ... on User { + risk + } + } + }, + }, + Flatten(path: "users.@") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + body + } + } + } + }, + }, + }, + }, + } + `); +}); diff --git a/gateway-js/src/__tests__/integration/mutations.test.ts b/gateway-js/src/__tests__/integration/mutations.test.ts new file mode 100644 index 000000000..fef0a5089 --- /dev/null +++ b/gateway-js/src/__tests__/integration/mutations.test.ts @@ -0,0 +1,284 @@ +import { execute } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; +import { accounts, reviews } from 'apollo-federation-integration-testsuite'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +function spyOnResolver(resolverMap: any, resolverName: T) { + return jest.spyOn(resolverMap, resolverName).mockName(resolverName); +} + +it('supports mutations', async () => { + const query = `#graphql + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + } + `; + + const variables = { username: '@complete', password: 'css_completes_me' }; + const { data, queryPlan } = await execute({ + query, + variables, + }); + + expect(data).toEqual({ + login: { + reviews: [ + { product: { upc: '3' } }, + { product: { upc: '1' } }, + { product: { upc: '0262510871' } }, + { product: { upc: '0136291554' } }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); +}); + +it('returning across service boundaries', async () => { + const query = `#graphql + mutation Review($upc: String!, $body: String!) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + `; + + const variables = { upc: '1', body: 'A great table' }; + const { data, queryPlan } = await execute({ + query, + variables, + }); + + expect(data).toEqual({ + reviewProduct: { + name: 'Table', + }, + }); + + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); +}); + +it('multiple root mutations', async () => { + const login = spyOnResolver(accounts.resolvers.Mutation, 'login'); + const reviewProduct = spyOnResolver( + reviews.resolvers.Mutation, + 'reviewProduct', + ); + + const query = `#graphql + mutation LoginAndReview( + $username: String! + $password: String! + $upc: String! + $body: String! + ) { + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + name + } + } + } + `; + + const variables = { + username: '@complete', + password: 'css_completes_me', + upc: '1', + body: 'A great table.', + }; + const { data, queryPlan } = await execute({ + query, + variables, + }); + + expect(data).toEqual({ + login: { + reviews: [ + { product: { upc: '3' } }, + { product: { upc: '1' } }, + { product: { upc: '0262510871' } }, + { product: { upc: '0136291554' } }, + ], + }, + reviewProduct: { + name: 'Table', + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + + expect(login).toHaveBeenCalledBefore(reviewProduct); +}); + +it('multiple root mutations with correct service order', async () => { + const reviewsMutations = reviews.resolvers.Mutation; + const reviewProduct = spyOnResolver(reviewsMutations, 'reviewProduct'); + const login = spyOnResolver(accounts.resolvers.Mutation, 'login'); + const updateReview = spyOnResolver(reviewsMutations, 'updateReview'); + const deleteReview = spyOnResolver(reviewsMutations, 'deleteReview'); + + const query = `#graphql + mutation LoginAndReview( + $upc: String! + $body: String! + $updatedReview: UpdateReviewInput! + $username: String! + $password: String! + $reviewId: ID! + ) { + reviewProduct(upc: $upc, body: $body) { + ... on Furniture { + upc + } + } + updateReview(review: $updatedReview) { + id + body + } + login(username: $username, password: $password) { + reviews { + product { + upc + } + } + } + deleteReview(id: $reviewId) + } + `; + + const variables = { + upc: '1', + body: 'A great table.', + updatedReview: { + id: '1', + body: 'An excellent table.', + }, + username: '@complete', + password: 'css_completes_me', + reviewId: '6', + }; + const { data, queryPlan } = await execute({ + query, + variables, + }); + + expect(data).toEqual({ + deleteReview: true, + login: { + reviews: [ + { product: { upc: '3' } }, + { product: { upc: '1' } }, + { product: { upc: '0262510871' } }, + { product: { upc: '0136291554' } }, + ], + }, + reviewProduct: { + upc: '1', + }, + updateReview: { + body: 'An excellent table.', + id: '1', + }, + }); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "reviews") { + { + reviewProduct(upc: $upc, body: $body) { + __typename + ... on Furniture { + upc + } + } + updateReview(review: $updatedReview) { + id + body + } + } + }, + Fetch(service: "accounts") { + { + login(username: $username, password: $password) { + __typename + id + } + } + }, + Flatten(path: "login") { + Fetch(service: "reviews") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + reviews { + product { + __typename + ... on Book { + __typename + isbn + } + ... on Furniture { + upc + } + } + } + } + } + }, + }, + Flatten(path: "login.reviews.@.product") { + Fetch(service: "product") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + upc + } + } + }, + }, + Fetch(service: "reviews") { + { + deleteReview(id: $reviewId) + } + }, + }, + } + `); + + expect(reviewProduct).toHaveBeenCalledBefore(updateReview); + expect(updateReview).toHaveBeenCalledBefore(login); + expect(login).toHaveBeenCalledBefore(deleteReview); +}); diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts new file mode 100644 index 000000000..5d3c10681 --- /dev/null +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -0,0 +1,472 @@ +import nock from 'nock'; +import { fetch } from 'apollo-server-env'; +import { Logger } from 'apollo-server-types'; +import { ApolloGateway, GCS_RETRY_COUNT, getDefaultGcsFetcher } from '../..'; +import { + mockSDLQuerySuccess, + mockServiceHealthCheckSuccess, + mockServiceHealthCheck, + mockStorageSecretSuccess, + mockStorageSecret, + mockCompositionConfigLinkSuccess, + mockCompositionConfigLink, + mockCompositionConfigsSuccess, + mockCompositionConfigs, + mockImplementingServicesSuccess, + mockImplementingServices, + mockRawPartialSchemaSuccess, + mockRawPartialSchema, + apiKeyHash, + graphId, +} from './nockMocks'; + +import loadServicesFromStorage = require("../../loadServicesFromStorage"); + +// This is a nice DX hack for GraphQL code highlighting and formatting within the file. +// Anything wrapped within the gql tag within this file is just a string, not an AST. +const gql = String.raw; + +export interface MockService { + gcsDefinitionPath: string; + partialSchemaPath: string; + url: string; + sdl: string; +} + +const service: MockService = { + gcsDefinitionPath: 'service-definition.json', + partialSchemaPath: 'accounts-partial-schema.json', + url: 'http://localhost:4001', + sdl: gql` + extend type Query { + me: User + everyone: [User] + } + + "This is my User" + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + `, +}; + +const updatedService: MockService = { + gcsDefinitionPath: 'updated-service-definition.json', + partialSchemaPath: 'updated-accounts-partial-schema.json', + url: 'http://localhost:4002', + sdl: gql` + extend type Query { + me: User + everyone: [User] + } + + "This is my updated User" + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + `, +}; + +let fetcher: typeof fetch; +let logger: Logger; + +beforeEach(() => { + if (!nock.isActive()) nock.activate(); + + fetcher = getDefaultGcsFetcher().defaults({ + retry: { + retries: GCS_RETRY_COUNT, + minTimeout: 0, + maxTimeout: 0, + }, + }); + + const warn = jest.fn(); + const debug = jest.fn(); + const error = jest.fn(); + const info = jest.fn(); + + logger = { + warn, + debug, + error, + info, + }; +}); + +afterEach(() => { + expect(nock.isDone()).toBeTruthy(); + nock.cleanAll(); + nock.restore(); +}); + +it('Queries remote endpoints for their SDLs', async () => { + mockSDLQuerySuccess(service); + + const gateway = new ApolloGateway({ + serviceList: [{ name: 'accounts', url: service.url }], + logger + }); + await gateway.load(); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); +}); + +it('Extracts service definitions from remote storage', async () => { + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + + const gateway = new ApolloGateway({ logger }); + + await gateway.load({ engine: { apiKeyHash, graphId } }); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); +}); + +it.each([ + ['warned', 'present'], + ['not warned', 'absent'], +])('conflicting configurations are %s about when %s', async (_word, mode) => { + const isConflict = mode === 'present'; + let blockerResolve: () => void; + const blocker = new Promise(resolve => (blockerResolve = resolve)); + const original = loadServicesFromStorage.getServiceDefinitionsFromStorage; + const spyGetServiceDefinitionsFromStorage = jest + .spyOn(loadServicesFromStorage, 'getServiceDefinitionsFromStorage') + .mockImplementationOnce(async (...args) => { + try { + return await original(...args); + } catch (e) { + throw e; + } finally { + setImmediate(blockerResolve); + } + }); + + mockStorageSecretSuccess(); + if (isConflict) { + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + } else { + mockCompositionConfigLink().reply(403); + } + + mockSDLQuerySuccess(service); + + const gateway = new ApolloGateway({ + serviceList: [ + { name: 'accounts', url: service.url }, + ], + logger + }); + + await gateway.load({ engine: { apiKeyHash, graphId } }); + await blocker; // Wait for the definitions to be "fetched". + + (isConflict + ? expect(logger.warn) + : expect(logger.warn).not + ).toHaveBeenCalledWith(expect.stringMatching( + /A local gateway service list is overriding an Apollo Graph Manager managed configuration/)); + spyGetServiceDefinitionsFromStorage.mockRestore(); +}); + +// This test has been flaky for a long time, and fails consistently after changes +// introduced by https://github.com/apollographql/apollo-server/pull/4277. +// I've decided to skip this test for now with hopes that we can one day +// determine the root cause and test this behavior in a reliable manner. +it.skip('Rollsback to a previous schema when triggered', async () => { + // Init + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + + // Update 1 + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([updatedService]); + mockImplementingServicesSuccess(updatedService); + mockRawPartialSchemaSuccess(updatedService); + + // Rollback + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServices(service).reply(304); + mockRawPartialSchema(service).reply(304); + + let firstResolve: () => void; + let secondResolve: () => void; + let thirdResolve: () => void + const firstSchemaChangeBlocker = new Promise(res => (firstResolve = res)); + const secondSchemaChangeBlocker = new Promise(res => (secondResolve = res)); + const thirdSchemaChangeBlocker = new Promise(res => (thirdResolve = res)); + + const onChange = jest + .fn() + .mockImplementationOnce(() => firstResolve()) + .mockImplementationOnce(() => secondResolve()) + .mockImplementationOnce(() => thirdResolve()); + + const gateway = new ApolloGateway({ logger }); + // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here + gateway.experimental_pollInterval = 100; + + gateway.onSchemaChange(onChange); + await gateway.load({ engine: { apiKeyHash, graphId } }); + + await firstSchemaChangeBlocker; + expect(onChange).toHaveBeenCalledTimes(1); + + await secondSchemaChangeBlocker; + expect(onChange).toHaveBeenCalledTimes(2); + + await thirdSchemaChangeBlocker; + expect(onChange).toHaveBeenCalledTimes(3); +}); + +function failNTimes(n: number, fn: () => nock.Interceptor) { + for (let i = 0; i < n; i++) { + fn().reply(500); + } +} + +it(`Retries GCS (up to ${GCS_RETRY_COUNT} times) on failure for each request and succeeds`, async () => { + failNTimes(GCS_RETRY_COUNT, mockStorageSecret); + mockStorageSecretSuccess(); + + failNTimes(GCS_RETRY_COUNT, mockCompositionConfigLink); + mockCompositionConfigLinkSuccess(); + + failNTimes(GCS_RETRY_COUNT, mockCompositionConfigs); + mockCompositionConfigsSuccess([service]); + + failNTimes(GCS_RETRY_COUNT, () => mockImplementingServices(service)); + mockImplementingServicesSuccess(service); + + failNTimes(GCS_RETRY_COUNT, () => mockRawPartialSchema(service)); + mockRawPartialSchemaSuccess(service); + + const gateway = new ApolloGateway({ fetcher, logger }); + + await gateway.load({ engine: { apiKeyHash, graphId } }); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); +}); + +// This test is reliably failing in its current form. It's mostly testing that +// `make-fetch-happen` is doing its retries properly and we have proof that, +// generally speaking, retries are working, so we'll disable this until we can +// re-visit it. +it.skip(`Fails after the ${GCS_RETRY_COUNT + 1}th attempt to reach GCS`, async () => { + failNTimes(GCS_RETRY_COUNT + 1, mockStorageSecret); + + const gateway = new ApolloGateway({ fetcher, logger }); + await expect( + gateway.load({ engine: { apiKeyHash, graphId } }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not communicate with Apollo Graph Manager storage: "`, + ); +}); + +it(`Errors when the secret isn't hosted on GCS`, async () => { + mockStorageSecret().reply( + 403, + `AccessDenied + Anonymous caller does not have storage.objects.get`, + { 'content-type': 'application/xml' }, + ); + + const gateway = new ApolloGateway({ fetcher, logger }); + await expect( + gateway.load({ engine: { apiKeyHash, graphId } }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to authenticate with Apollo Graph Manager storage while fetching https://storage-secrets.api.apollographql.com/federated-service/storage-secret/dd55a79d467976346d229a7b12b673ce.json. Ensure that the API key is configured properly and that a federated service has been pushed. For details, see https://go.apollo.dev/g/resolve-access-denied."`, + ); +}); + +describe('Downstream service health checks', () => { + describe('Unmanaged mode', () => { + it(`Performs health checks to downstream services on load`, async () => { + mockSDLQuerySuccess(service); + mockServiceHealthCheckSuccess(service); + + const gateway = new ApolloGateway({ + logger, + serviceList: [{ name: 'accounts', url: service.url }], + serviceHealthCheck: true, + }); + + await gateway.load(); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); + }); + + it(`Rejects on initial load when health check fails`, async () => { + mockSDLQuerySuccess(service); + mockServiceHealthCheck(service).reply(500); + + const gateway = new ApolloGateway({ + serviceList: [{ name: 'accounts', url: service.url }], + serviceHealthCheck: true, + logger, + }); + + await expect(gateway.load()).rejects.toThrowErrorMatchingInlineSnapshot( + `"500: Internal Server Error"`, + ); + }); + }); + + describe('Managed mode', () => { + it('Performs health checks to downstream services on load', async () => { + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + + mockServiceHealthCheckSuccess(service); + + const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); + + await gateway.load({ engine: { apiKeyHash, graphId } }); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); + }); + + it('Rejects on initial load when health check fails', async () => { + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + + mockServiceHealthCheck(service).reply(500); + + const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); + + await expect( + gateway.load({ engine: { apiKeyHash, graphId } }), + ).rejects.toThrowErrorMatchingInlineSnapshot(`"500: Internal Server Error"`); + }); + + // This test has been flaky for a long time, and fails consistently after changes + // introduced by https://github.com/apollographql/apollo-server/pull/4277. + // I've decided to skip this test for now with hopes that we can one day + // determine the root cause and test this behavior in a reliable manner. + it.skip('Rolls over to new schema when health check succeeds', async () => { + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + mockServiceHealthCheckSuccess(service); + + // Update + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([updatedService]); + mockImplementingServicesSuccess(updatedService); + mockRawPartialSchemaSuccess(updatedService); + mockServiceHealthCheckSuccess(updatedService); + + let resolve1: () => void; + let resolve2: () => void; + const schemaChangeBlocker1 = new Promise(res => (resolve1 = res)); + const schemaChangeBlocker2 = new Promise(res => (resolve2 = res)); + const onChange = jest + .fn() + .mockImplementationOnce(() => resolve1()) + .mockImplementationOnce(() => resolve2()); + + const gateway = new ApolloGateway({ + serviceHealthCheck: true, + logger, + }); + // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here + gateway.experimental_pollInterval = 100; + + gateway.onSchemaChange(onChange); + await gateway.load({ engine: { apiKeyHash, graphId } }); + + await schemaChangeBlocker1; + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); + expect(onChange).toHaveBeenCalledTimes(1); + + await schemaChangeBlocker2; + expect(gateway.schema!.getType('User')!.description).toBe('This is my updated User'); + expect(onChange).toHaveBeenCalledTimes(2); + }); + + it('Preserves original schema when health check fails', async () => { + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([service]); + mockImplementingServicesSuccess(service); + mockRawPartialSchemaSuccess(service); + mockServiceHealthCheckSuccess(service); + + // Update + mockStorageSecretSuccess(); + mockCompositionConfigLinkSuccess(); + mockCompositionConfigsSuccess([updatedService]); + mockImplementingServicesSuccess(updatedService); + mockRawPartialSchemaSuccess(updatedService); + mockServiceHealthCheck(updatedService).reply(500); + + let resolve: () => void; + const schemaChangeBlocker = new Promise(res => (resolve = res)); + + const gateway = new ApolloGateway({ serviceHealthCheck: true, logger }); + // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here + gateway.experimental_pollInterval = 100; + + // @ts-ignore for testing purposes, we'll call the original `updateComposition` + // function from our mock. The first call should mimic original behavior, + // but the second call needs to handle the PromiseRejection. Typically for tests + // like these we would leverage the `gateway.onSchemaChange` callback to drive + // the test, but in this case, that callback isn't triggered when the update + // fails (as expected) so we get creative with the second mock as seen below. + const original = gateway.updateComposition; + const mockUpdateComposition = jest + .fn() + .mockImplementationOnce(async () => { + await original.apply(gateway); + }) + .mockImplementationOnce(async () => { + // mock the first poll and handle the error which would otherwise be caught + // and logged from within the `pollServices` class method + await expect(original.apply(gateway)) + .rejects + .toThrowErrorMatchingInlineSnapshot( + `"500: Internal Server Error"`, + ); + // finally resolve the promise which drives this test + resolve(); + }); + + // @ts-ignore for testing purposes, replace the `updateComposition` + // function on the gateway with our mock + gateway.updateComposition = mockUpdateComposition; + + // load the gateway as usual + await gateway.load({ engine: { apiKeyHash, graphId } }); + + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); + + await schemaChangeBlocker; + + // At this point, the mock update should have been called but the schema + // should not have updated to the new one. + expect(mockUpdateComposition.mock.calls.length).toBe(2); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); + }); + }); +}); diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts new file mode 100644 index 000000000..3ad391b71 --- /dev/null +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -0,0 +1,113 @@ +import nock from 'nock'; +import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..'; +import { MockService } from './networkRequests.test'; + +export const graphId = 'federated-service'; +export const apiKeyHash = 'dd55a79d467976346d229a7b12b673ce'; +const storageSecret = 'my-storage-secret'; +const accountsService = 'accounts'; + +// Service mocks +function mockSDLQuery({ url }: MockService) { + return nock(url).post('/', { + query: SERVICE_DEFINITION_QUERY, + }); +} + +export function mockSDLQuerySuccess(service: MockService) { + mockSDLQuery(service).reply(200, { + data: { _service: { sdl: service.sdl } }, + }); +} + +export function mockServiceHealthCheck({ url }: MockService) { + return nock(url).post('/', { + query: HEALTH_CHECK_QUERY, + }); +} + +export function mockServiceHealthCheckSuccess(service: MockService) { + return mockServiceHealthCheck(service).reply(200, { + data: { __typename: 'Query' }, + }); +} + +// GCS mocks +function gcsNock(url: Parameters[0]): nock.Scope { + return nock(url, { + reqheaders: { + 'user-agent': `apollo-gateway/${ + require('../../../package.json').version + }`, + }, + }); +} + +export function mockStorageSecret() { + return gcsNock('https://storage-secrets.api.apollographql.com:443').get( + `/${graphId}/storage-secret/${apiKeyHash}.json`, + ); +} + +export function mockStorageSecretSuccess() { + return gcsNock('https://storage-secrets.api.apollographql.com:443') + .get( + `/${graphId}/storage-secret/${apiKeyHash}.json`, + ) + .reply(200, `"${storageSecret}"`); +} + +// get composition config link, using received storage secret +export function mockCompositionConfigLink() { + return gcsNock('https://federation.api.apollographql.com:443').get( + `/${storageSecret}/current/v1/composition-config-link`, + ); +} + +export function mockCompositionConfigLinkSuccess() { + return mockCompositionConfigLink().reply(200, { + configPath: `${storageSecret}/current/v1/composition-configs/composition-config-path.json`, + }); +} + +// get composition configs, using received composition config link +export function mockCompositionConfigs() { + return gcsNock('https://federation.api.apollographql.com:443').get( + `/${storageSecret}/current/v1/composition-configs/composition-config-path.json`, + ); +} + +export function mockCompositionConfigsSuccess(services: MockService[]) { + return mockCompositionConfigs().reply(200, { + implementingServiceLocations: services.map(service => ({ + name: accountsService, + path: `${storageSecret}/current/v1/implementing-services/${accountsService}/${service.gcsDefinitionPath}`, + })), + }); +} + +// get implementing service reference, using received composition-config +export function mockImplementingServices({ gcsDefinitionPath }: MockService) { + return gcsNock('https://federation.api.apollographql.com:443').get( + `/${storageSecret}/current/v1/implementing-services/${accountsService}/${gcsDefinitionPath}`, + ); +} + +export function mockImplementingServicesSuccess(service: MockService) { + return mockImplementingServices(service).reply(200, { + name: accountsService, + partialSchemaPath: `${storageSecret}/current/raw-partial-schemas/${service.partialSchemaPath}`, + url: service.url, + }); +} + +// get raw-partial-schema, using received composition-config +export function mockRawPartialSchema({ partialSchemaPath }: MockService) { + return gcsNock('https://federation.api.apollographql.com:443').get( + `/${storageSecret}/current/raw-partial-schemas/${partialSchemaPath}`, + ); +} + +export function mockRawPartialSchemaSuccess(service: MockService) { + return mockRawPartialSchema(service).reply(200, service.sdl); +} diff --git a/gateway-js/src/__tests__/integration/provides.test.ts b/gateway-js/src/__tests__/integration/provides.test.ts new file mode 100644 index 000000000..14c925139 --- /dev/null +++ b/gateway-js/src/__tests__/integration/provides.test.ts @@ -0,0 +1,77 @@ +import { execute, overrideResolversInService } from '../execution-utils'; +import { fixtures } from 'apollo-federation-integration-testsuite'; + +it('does not have to go to another service when field is given', async () => { + const query = `#graphql + query GetReviewers { + topReviews { + author { + username + } + } + } + `; + + const { data, queryPlan } = await execute( { + query, + }); + + expect(data).toEqual({ + topReviews: [ + { author: { username: '@ada' } }, + { author: { username: '@ada' } }, + { author: { username: '@complete' } }, + { author: { username: '@complete' } }, + { author: { username: '@complete' } }, + ], + }); + + expect(queryPlan).not.toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); +}); + +it('does not load fields provided even when going to other service', async () => { + const [accounts, ...restFixtures] = fixtures; + + const username = jest.fn(); + const localAccounts = overrideResolversInService(accounts, { + User: { + username, + }, + }); + + const query = `#graphql + query GetReviewers { + topReviews { + author { + username + name { + first + last + } + } + } + } + `; + + const { data, queryPlan } = await execute( + { + query, + }, + [localAccounts, ...restFixtures], + ); + + expect(data).toEqual({ + topReviews: [ + { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, + { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, + { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, + { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, + { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, + ], + }); + + expect(username).not.toHaveBeenCalled(); + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); +}); diff --git a/gateway-js/src/__tests__/integration/requires.test.ts b/gateway-js/src/__tests__/integration/requires.test.ts new file mode 100644 index 000000000..1ea07cce2 --- /dev/null +++ b/gateway-js/src/__tests__/integration/requires.test.ts @@ -0,0 +1,357 @@ +import gql from 'graphql-tag'; +import { execute } from '../execution-utils'; +import { serializeQueryPlan } from '../..'; + +it('supports passing additional fields defined by a requires', async () => { + const query = `#graphql + query GetReviwedBookNames { + me { + reviews { + product { + ... on Book { + name + } + } + } + } + } + `; + + const { data, queryPlan } = await execute({ + query, + }); + + expect(data).toEqual({ + me: { + reviews: [ + { product: {} }, + { product: {} }, + { + product: { + name: 'Design Patterns (1995)', + }, + }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('books'); +}); + +const serviceA = { + name: 'a', + typeDefs: gql` + type Query { + user: User + } + + type User @key(fields: "id") { + id: ID! + preferences: Preferences + } + + type Preferences { + favorites: Things + } + + type Things { + color: String + animal: String + } + `, + resolvers: { + Query: { + user() { + return { + id: '1', + preferences: { + favorites: { color: 'limegreen', animal: 'platypus' }, + }, + }; + }, + }, + }, +}; + +const serviceB = { + name: 'b', + typeDefs: gql` + extend type User @key(fields: "id") { + id: ID! @external + preferences: Preferences @external + favoriteColor: String + @requires(fields: "preferences { favorites { color } }") + favoriteAnimal: String + @requires(fields: "preferences { favorites { animal } }") + } + + extend type Preferences { + favorites: Things @external + } + + extend type Things { + color: String @external + animal: String @external + } + `, + resolvers: { + User: { + favoriteColor(user: any) { + return user.preferences.favorites.color; + }, + favoriteAnimal(user: any) { + return user.preferences.favorites.animal; + }, + }, + }, +}; + +it('collapses nested requires', async () => { + const query = `#graphql + query UserFavorites { + user { + favoriteColor + favoriteAnimal + } + } + `; + + const { data, errors, queryPlan } = await execute( + { + query, + }, + [serviceA, serviceB], + ); + + expect(errors).toEqual(undefined); + + expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(` + "QueryPlan { + Sequence { + Fetch(service: \\"a\\") { + { + user { + __typename + id + preferences { + favorites { + color + animal + } + } + } + } + }, + Flatten(path: \\"user\\") { + Fetch(service: \\"b\\") { + { + ... on User { + __typename + id + preferences { + favorites { + color + animal + } + } + } + } => + { + ... on User { + favoriteColor + favoriteAnimal + } + } + }, + }, + }, + }" + `); + + expect(data).toEqual({ + user: { + favoriteAnimal: 'platypus', + favoriteColor: 'limegreen', + }, + }); + + expect(queryPlan).toCallService('a'); + expect(queryPlan).toCallService('b'); +}); + +it('collapses nested requires with user-defined fragments', async () => { + const query = `#graphql + query UserFavorites { + user { + favoriteAnimal + ...favoriteColor + } + } + + fragment favoriteColor on User { + preferences { + favorites { + color + } + } + } + `; + + const { data, errors, queryPlan } = await execute( + { + query, + }, + [serviceA, serviceB], + ); + + expect(errors).toEqual(undefined); + + expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(` + "QueryPlan { + Sequence { + Fetch(service: \\"a\\") { + { + user { + __typename + id + preferences { + favorites { + animal + color + } + } + } + } + }, + Flatten(path: \\"user\\") { + Fetch(service: \\"b\\") { + { + ... on User { + __typename + id + preferences { + favorites { + animal + } + } + } + } => + { + ... on User { + favoriteAnimal + } + } + }, + }, + }, + }" + `); + + expect(data).toEqual({ + user: { + favoriteAnimal: 'platypus', + preferences: { + favorites: { + color: 'limegreen', + }, + }, + }, + }); + + expect(queryPlan).toCallService('a'); + expect(queryPlan).toCallService('b'); +}); + +it('passes null values correctly', async () => { + const serviceA = { + name: 'a', + typeDefs: gql` + type Query { + user: User + } + + type User @key(fields: "id") { + id: ID! + favorite: Color + dislikes: [Color] + } + + type Color { + name: String! + } + `, + resolvers: { + Query: { + user() { + return { + id: '1', + favorite: null, + dislikes: [null], + }; + }, + }, + }, + }; + + const serviceB = { + name: 'b', + typeDefs: gql` + extend type User @key(fields: "id") { + id: ID! @external + favorite: Color @external + dislikes: [Color] @external + favoriteColor: String @requires(fields: "favorite { name }") + dislikedColors: String @requires(fields: "dislikes { name }") + } + + extend type Color { + name: String! @external + } + `, + resolvers: { + User: { + favoriteColor(user: any) { + if (user.favorite !== null) { + throw Error( + 'Favorite color should be null. Instead, got: ' + + JSON.stringify(user.favorite), + ); + } + return 'unknown'; + }, + dislikedColors(user: any) { + const color = user.dislikes[0]; + if (color !== null) { + throw Error( + 'Disliked colors should be null. Instead, got: ' + + JSON.stringify(user.dislikes), + ); + } + return 'unknown'; + }, + }, + }, + }; + + const query = `#graphql + query UserFavorites { + user { + favoriteColor + dislikedColors + } + } + `; + + const { data, errors } = await execute({ query }, [serviceA, serviceB]); + + expect(errors).toEqual(undefined); + expect(data).toEqual({ + user: { + favoriteColor: 'unknown', + dislikedColors: 'unknown', + }, + }); +}); diff --git a/gateway-js/src/__tests__/integration/single-service.test.ts b/gateway-js/src/__tests__/integration/single-service.test.ts new file mode 100644 index 000000000..53ab513b0 --- /dev/null +++ b/gateway-js/src/__tests__/integration/single-service.test.ts @@ -0,0 +1,119 @@ +import gql from 'graphql-tag'; +import { execute, overrideResolversInService } from '../execution-utils'; + +const accounts = { + name: 'accounts', + typeDefs: gql` + type User @key(fields: "id") { + id: Int! + name: String + account: Account + } + type Account { + type: String + } + extend type Query { + me: User + } + `, + resolvers: { + Query: { + me: () => ({ id: 1, name: 'Martijn' }), + }, + }, +}; + +it('executes a query plan over concrete types', async () => { + const me = jest.fn(() => ({ id: 1, name: 'James' })); + const localAccounts = overrideResolversInService(accounts, { + Query: { me }, + }); + + const query = `#graphql + query GetUser { + me { + id + name + } + } + `; + const { data, queryPlan } = await execute( + { + query, + }, + [localAccounts], + ); + + expect(data).toEqual({ me: { id: 1, name: 'James' } }); + expect(queryPlan).toCallService('accounts'); + expect(me).toBeCalled(); +}); + +it('does not remove __typename on root types', async () => { + const query = `#graphql + query GetUser { + __typename + } + `; + + const { data } = await execute( + { + query, + }, + [accounts], + ); + + expect(data).toEqual({ __typename: 'Query' }); +}); + +it('does not remove __typename if that is all that is requested on an entity', async () => { + const me = jest.fn(() => ({ id: 1, name: 'James' })); + const localAccounts = overrideResolversInService(accounts, { + Query: { me }, + }); + + const query = `#graphql + query GetUser { + me { + __typename + } + } + `; + const { data, queryPlan } = await execute( + { + query, + }, + [localAccounts], + ); + + expect(data).toEqual({ me: { __typename: 'User' } }); + expect(queryPlan).toCallService('accounts'); + expect(me).toBeCalled(); +}); + +it('does not remove __typename if that is all that is requested on a value type', async () => { + const me = jest.fn(() => ({ id: 1, name: 'James', account: {} })); + const localAccounts = overrideResolversInService(accounts, { + Query: { me }, + }); + + const query = `#graphql + query GetUser { + me { + account { + __typename + } + } + } + `; + const { data, queryPlan } = await execute( + { + query, + }, + [localAccounts], + ); + + expect(data).toEqual({ me: { account: { __typename: 'Account' } } }); + expect(queryPlan).toCallService('accounts'); + expect(me).toBeCalled(); +}); diff --git a/gateway-js/src/__tests__/integration/unions.test.ts b/gateway-js/src/__tests__/integration/unions.test.ts new file mode 100644 index 000000000..42827bb35 --- /dev/null +++ b/gateway-js/src/__tests__/integration/unions.test.ts @@ -0,0 +1,79 @@ +import gql from 'graphql-tag'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; +import { execute } from '../execution-utils'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +it('handles multiple union type conditions that share a response name (media)', async () => { + const query = `#graphql + query { + content { + ...Audio + ... on Video { + media { + aspectRatio + } + } + } + } + fragment Audio on Audio { + media { + url + } + } + `; + + const { queryPlan, errors } = await execute( + { query }, + [ + { + name: 'contentService', + typeDefs: gql` + extend type Query { + content: Content + } + union Content = Audio | Video + type Audio { + media: AudioURL + } + type AudioURL { + url: String + } + type Video { + media: VideoAspectRatio + } + type VideoAspectRatio { + aspectRatio: String + } + `, + resolvers: { + Query: {}, + }, + }, + ], + ); + + expect(errors).toBeUndefined(); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "contentService") { + { + content { + __typename + ... on Audio { + media { + url + } + } + ... on Video { + media { + aspectRatio + } + } + } + } + }, + } + `); +}); diff --git a/gateway-js/src/__tests__/integration/value-types.test.ts b/gateway-js/src/__tests__/integration/value-types.test.ts new file mode 100644 index 000000000..58064ce17 --- /dev/null +++ b/gateway-js/src/__tests__/integration/value-types.test.ts @@ -0,0 +1,382 @@ +import gql from 'graphql-tag'; +import { execute } from '../execution-utils'; +import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers'; + +expect.addSnapshotSerializer(astSerializer); +expect.addSnapshotSerializer(queryPlanSerializer); + +describe('value types', () => { + it('resolves value types within their respective services', async () => { + const query = `#graphql + fragment Metadata on MetadataOrError { + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + + query ProducsWithMetadata { + topProducts(first: 10) { + upc + ... on Book { + metadata { + ...Metadata + } + } + ... on Furniture { + metadata { + ...Metadata + } + } + reviews { + metadata { + ...Metadata + } + } + } + } + `; + + const { data, errors, queryPlan } = await execute({ + query, + }); + + expect(errors).toBeUndefined(); + + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "product") { + { + topProducts(first: 10) { + __typename + ... on Book { + upc + __typename + isbn + } + ... on Furniture { + upc + metadata { + __typename + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + __typename + } + } + } + }, + Parallel { + Flatten(path: "topProducts.@") { + Fetch(service: "books") { + { + ... on Book { + __typename + isbn + } + } => + { + ... on Book { + metadata { + __typename + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + } + } + }, + }, + Flatten(path: "topProducts.@") { + Fetch(service: "reviews") { + { + ... on Book { + __typename + isbn + } + ... on Furniture { + __typename + upc + } + } => + { + ... on Book { + reviews { + metadata { + __typename + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + } + } + ... on Furniture { + reviews { + metadata { + __typename + ... on KeyValue { + key + value + } + ... on Error { + code + message + } + } + } + } + } + }, + }, + }, + }, + } + `); + + const [furniture, , , , book] = data!.topProducts; + + // Sanity check, referenceable ID + expect(furniture.upc).toEqual('1'); + // Value type resolves from the correct service + expect(furniture.metadata[0]).toEqual({ + key: 'Condition', + value: 'excellent', + }); + + // Value type from a different service (reviews) also resolves correctly + expect(furniture.reviews[0].metadata[0]).toEqual({ + code: 418, + message: "I'm a teapot", + }); + + // Sanity check, referenceable ID + expect(book.upc).toEqual('0136291554'); + // Value type as a union resolves correctly + expect(book.metadata).toEqual([ + { + key: 'Condition', + value: 'used', + }, + { + code: 401, + message: 'Unauthorized', + }, + ]); + + expect(queryPlan).toCallService('product'); + expect(queryPlan).toCallService('books'); + expect(queryPlan).toCallService('reviews'); + }); + + it('resolves @provides fields on value types correctly via contrived example', async () => { + const firstService = { + name: 'firstService', + typeDefs: gql` + extend type Query { + valueType: ValueType + } + + type ValueType { + id: ID! + user: User! @provides(fields: "id name") + } + + extend type User @key(fields: "id") { + id: ID! @external + name: String! @external + } + `, + resolvers: { + Query: { + valueType() { + return { id: '123', user: { id: '1', name: 'trevor' } }; + }, + }, + }, + }; + + const secondService = { + name: 'secondService', + typeDefs: gql` + extend type Query { + otherValueType: ValueType + } + + type ValueType { + id: ID! + user: User! @provides(fields: "id name") + } + + extend type User @key(fields: "id") { + id: ID! @external + name: String! @external + } + `, + resolvers: { + Query: { + otherValueType() { + return { id: '456', user: { id: '2', name: 'james' } }; + }, + }, + }, + }; + + const userService = { + name: 'userService', + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + name: String! + address: String! + } + `, + resolvers: { + User: { + __resolveReference(user: any) { + return user.id === '1' + ? { id: '1', name: 'trevor', address: '123 Abc St' } + : { id: '2', name: 'james', address: '456 Hello St.' }; + }, + }, + }, + }; + + const query = `#graphql + query Hello { + valueType { + id + user { + id + name + address + } + } + otherValueType { + id + user { + id + name + address + } + } + } + `; + + const { data, errors, queryPlan } = await execute( + { + query, + }, + [firstService, secondService, userService], + ); + + expect(errors).toBeUndefined(); + expect(queryPlan).toCallService('firstService'); + expect(queryPlan).toCallService('secondService'); + expect(queryPlan).toCallService('userService'); + expect(data).toMatchInlineSnapshot(` + Object { + "otherValueType": Object { + "id": "456", + "user": Object { + "address": "456 Hello St.", + "id": "2", + "name": "james", + }, + }, + "valueType": Object { + "id": "123", + "user": Object { + "address": "123 Abc St", + "id": "1", + "name": "trevor", + }, + }, + } + `); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Parallel { + Sequence { + Fetch(service: "firstService") { + { + valueType { + id + user { + id + name + __typename + } + } + } + }, + Flatten(path: "valueType.user") { + Fetch(service: "userService") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + address + } + } + }, + }, + }, + Sequence { + Fetch(service: "secondService") { + { + otherValueType { + id + user { + id + name + __typename + } + } + } + }, + Flatten(path: "otherValueType.user") { + Fetch(service: "userService") { + { + ... on User { + __typename + id + } + } => + { + ... on User { + address + } + } + }, + }, + }, + }, + } + `); + }); +}); diff --git a/gateway-js/src/__tests__/integration/variables.test.ts b/gateway-js/src/__tests__/integration/variables.test.ts new file mode 100644 index 000000000..15209f44f --- /dev/null +++ b/gateway-js/src/__tests__/integration/variables.test.ts @@ -0,0 +1,120 @@ +import { execute } from '../execution-utils'; + +it('passes variables to root fields', async () => { + const query = `#graphql + query GetProduct($upc: String!) { + product(upc: $upc) { + name + } + } + `; + + const upc = '1'; + const { data, errors, queryPlan } = await execute({ + query, + variables: { upc }, + }); + + expect(errors).toBeUndefined(); + expect(data).toEqual({ + product: { + name: 'Table', + }, + }); + + expect(queryPlan).toCallService('product'); +}); + +it('supports default variables in a variable definition', async () => { + const query = `#graphql + query GetProduct($upc: String = "1") { + product(upc: $upc) { + name + } + } + `; + + const { data, errors, queryPlan } = await execute({ + query, + }); + + expect(errors).toBeUndefined(); + expect(data).toEqual({ + product: { + name: 'Table', + }, + }); + + expect(queryPlan).toCallService('product'); +}); + +it('passes variables to nested services', async () => { + const query = `#graphql + query GetProductsForUser($format: Boolean) { + me { + reviews { + body(format: $format) + } + } + } + `; + + const format = true; + const { data, errors, queryPlan } = await execute({ + query, + variables: { format }, + }); + + expect(errors).toBeUndefined(); + expect(data).toEqual({ + me: { + reviews: [ + { body: 'Love it!' }, + { body: 'Too expensive.' }, + { + body: 'A classic.', + }, + ], + }, + }); + + expect(queryPlan).toCallService('accounts'); + expect(queryPlan).toCallService('reviews'); +}); + +it('works with default variables in the schema', async () => { + const query = `#graphql + query LibraryUser($libraryId: ID!, $userId: ID) { + library(id: $libraryId) { + userAccount(id: $userId) { + id + name { + first + last + } + } + } + } + `; + + const { data, queryPlan, errors } = await execute({ + query, + variables: { libraryId: '1' }, + }); + + expect(data).toEqual({ + library: { + userAccount: { + id: '1', + name: { + first: 'Ada', + last: 'Lovelace', + } + }, + }, + }); + + expect(errors).toBeUndefined(); + expect(queryPlan).toCallService('books'); + expect(queryPlan).toCallService('accounts'); +}); diff --git a/gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts new file mode 100644 index 000000000..85d89c125 --- /dev/null +++ b/gateway-js/src/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -0,0 +1,36 @@ +import { getServiceDefinitionsFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint'; +import { RemoteGraphQLDataSource } from '../datasources'; + +describe('getServiceDefinitionsFromRemoteEndpoint', () => { + it('errors when no URL was specified', async () => { + const serviceSdlCache = new Map(); + const dataSource = new RemoteGraphQLDataSource({ url: '' }); + const serviceList = [{ name: 'test', dataSource }]; + await expect( + getServiceDefinitionsFromRemoteEndpoint({ + serviceList, + serviceSdlCache, + }), + ).rejects.toThrowError( + "Tried to load schema for 'test' but no 'url' was specified.", + ); + }); + + it('throws when the downstream service returns errors', async () => { + const serviceSdlCache = new Map(); + const host = 'http://host-which-better-not-resolve'; + const url = host + '/graphql'; + + const dataSource = new RemoteGraphQLDataSource({ url }); + const serviceList = [{ name: 'test', url, dataSource }]; + // Depending on the OS's resolver, the error may result in an error + // of `EAI_AGAIN` or `ENOTFOUND`. This `toThrowError` uses a Regex + // to match either case. + await expect( + getServiceDefinitionsFromRemoteEndpoint({ + serviceList, + serviceSdlCache, + }), + ).rejects.toThrowError(/^Couldn't load service definitions for "test" at http:\/\/host-which-better-not-resolve\/graphql: request to http:\/\/host-which-better-not-resolve\/graphql failed, reason: getaddrinfo (ENOTFOUND|EAI_AGAIN)/); + }); +}); diff --git a/gateway-js/src/__tests__/matchers/toCallService.ts b/gateway-js/src/__tests__/matchers/toCallService.ts new file mode 100644 index 000000000..1640861fa --- /dev/null +++ b/gateway-js/src/__tests__/matchers/toCallService.ts @@ -0,0 +1,105 @@ +import { QueryPlan } from '@apollo/gateway'; +import { PlanNode } from '../../QueryPlan'; +import astSerializer from '../../snapshotSerializers/astSerializer'; +import queryPlanSerializer from '../../snapshotSerializers/queryPlanSerializer'; +const prettyFormat = require('pretty-format'); + +declare global { + namespace jest { + interface Matchers { + toCallService(service: string): R; + } + } +} + +// function printNode(node: ExecutionNode) { +// return prettyFormat( +// { nodes: [node], kind: 'QueryPlan' }, +// { +// plugins: [queryPlanSerializer, astSerializer], +// }, +// ); +// } + +const lineEndRegex = /^/gm; +function indentString(string: string, count = 2) { + if (!string) return string; + return string.replace(lineEndRegex, ' '.repeat(count)); +} + +function toCallService( + this: jest.MatcherUtils, + queryPlan: QueryPlan, + service: string, +): { message(): string; pass: boolean } { + // const receivedString = print(received); + // const expectedString = print(expected); + + const printReceived = (string: string) => + this.utils.RECEIVED_COLOR(indentString(string)); + const printExpected = (string: string) => + this.utils.EXPECTED_COLOR(indentString(string)); + + let pass = false; + // let initialServiceCall = null; + // recurse the node, find first match of service name, return + function walkExecutionNode(node?: PlanNode) { + if (!node) return; + if (node.kind === 'Fetch' && node.serviceName === service) { + pass = true; + // initialServiceCall = node; + return; + } + switch (node.kind) { + case 'Flatten': + walkExecutionNode(node.node); + break; + case 'Parallel': + case 'Sequence': + node.nodes.forEach(walkExecutionNode); + break; + default: + return; + } + } + + walkExecutionNode(queryPlan.node); + + const message = pass + ? () => + this.utils.matcherHint('.not.toCallService') + + '\n\n' + + `Expected query plan to not call service:\n` + + printExpected(service) + + '\n' + + `Received:\n` + + // FIXME print just the node + printReceived( + prettyFormat(queryPlan, { + plugins: [queryPlanSerializer, astSerializer], + }), + ) + : () => { + return ( + this.utils.matcherHint('.toCallService') + + '\n\n' + + `Expected query plan to call service:\n` + + printExpected(service) + + '\n' + + `Received query plan:\n` + + printReceived( + prettyFormat(queryPlan, { + plugins: [queryPlanSerializer, astSerializer], + }), + ) + ); + }; + return { + message, + pass, + }; +} + +expect.extend({ + toCallService, +}); diff --git a/gateway-js/src/__tests__/matchers/toHaveBeenCalledBefore.ts b/gateway-js/src/__tests__/matchers/toHaveBeenCalledBefore.ts new file mode 100644 index 000000000..1b5f86c64 --- /dev/null +++ b/gateway-js/src/__tests__/matchers/toHaveBeenCalledBefore.ts @@ -0,0 +1,40 @@ +// Make this file a module +// See: https://github.com/microsoft/TypeScript/issues/17736 +export {}; +declare global { + namespace jest { + interface Matchers { + toHaveBeenCalledBefore(spy: SpyInstance): R; + } + } +} + +function toHaveBeenCalledBefore( + this: jest.MatcherUtils, + firstSpy: jest.SpyInstance, + secondSpy: jest.SpyInstance, +): { message(): string; pass: boolean } { + const firstSpyEarliestCall = Math.min(...firstSpy.mock.invocationCallOrder); + const secondSpyEarliestCall = Math.min(...secondSpy.mock.invocationCallOrder); + + const pass = firstSpyEarliestCall < secondSpyEarliestCall; + + const message = pass + ? () => + this.utils.matcherHint('.not.toHaveBeenCalledBefore') + + '\n\n' + + `Expected ${firstSpy.getMockName()} not to have been called before ${secondSpy.getMockName()}` + : () => + this.utils.matcherHint('.toHaveBeenCalledBefore') + + '\n\n' + + `Expected ${firstSpy.getMockName()} to have been called before ${secondSpy.getMockName()}`; + + return { + message, + pass, + }; +} + +expect.extend({ + toHaveBeenCalledBefore, +}); diff --git a/gateway-js/src/__tests__/matchers/toHaveFetched.ts b/gateway-js/src/__tests__/matchers/toHaveFetched.ts new file mode 100644 index 000000000..4ebbcc24b --- /dev/null +++ b/gateway-js/src/__tests__/matchers/toHaveFetched.ts @@ -0,0 +1,81 @@ +import { Request, RequestInit, Headers } from 'apollo-server-env'; + +// Make this file a module +// See: https://github.com/microsoft/TypeScript/issues/17736 +export {}; +declare global { + namespace jest { + interface Matchers { + toHaveFetched(spy: SpyInstance): R; + } + } +} + +type ExtendedRequest = RequestInit & { url: string }; + +function prepareHttpRequest(request: ExtendedRequest): Request { + const headers = new Headers(); + headers.set('Content-Type', 'application/json'); + if (request.headers) { + for (let name in request.headers) { + headers.set(name, request.headers[name]); + } + } + + const options: RequestInit = { + method: 'POST', + headers, + body: JSON.stringify(request.body), + }; + + return new Request(request.url, options); +} + +function toHaveFetched( + this: jest.MatcherUtils, + fetch: jest.SpyInstance, + request: ExtendedRequest, +): { message(): string; pass: boolean } { + const httpRequest = prepareHttpRequest(request); + let pass = false; + let message = () => ''; + try { + expect(fetch).toBeCalledWith(httpRequest); + pass = true; + } catch (e) { + message = () => e.message; + } + + return { + message, + pass, + }; +} + +function toHaveFetchedNth( + this: jest.MatcherUtils, + fetch: jest.SpyInstance, + nthCall: number, + request: ExtendedRequest, +): { message(): string; pass: boolean } { + const httpRequest = prepareHttpRequest(request); + let pass = false; + let message = () => ''; + try { + expect(fetch).toHaveBeenNthCalledWith(nthCall, httpRequest); + pass = true; + } catch (e) { + message = () => e.message; + } + + return { + message, + pass, + }; +} + + +expect.extend({ + toHaveFetched, + toHaveFetchedNth, +}); diff --git a/gateway-js/src/__tests__/matchers/toMatchAST.ts b/gateway-js/src/__tests__/matchers/toMatchAST.ts new file mode 100644 index 000000000..8df057e7c --- /dev/null +++ b/gateway-js/src/__tests__/matchers/toMatchAST.ts @@ -0,0 +1,64 @@ +import { print, ASTNode } from 'graphql'; +const diff = require('jest-diff'); + +declare global { + namespace jest { + interface Matchers { + toMatchAST(expected: ASTNode): R; + } + } +} + +const lineEndRegex = /^/gm; +function indentString(string: string, count = 2) { + if (!string) return string; + return string.replace(lineEndRegex, ' '.repeat(count)); +} + +function toMatchAST( + this: jest.MatcherUtils, + received: ASTNode, + expected: ASTNode, +): { message(): string; pass: boolean } { + const receivedString = print(received); + const expectedString = print(expected); + + const printReceived = (string: string) => + this.utils.RECEIVED_COLOR(indentString(string)); + const printExpected = (string: string) => + this.utils.EXPECTED_COLOR(indentString(string)); + + const pass = this.equals(receivedString, expectedString); + const message = pass + ? () => + this.utils.matcherHint('.not.toMatchAST') + + '\n\n' + + `Expected AST to not equal:\n` + + printExpected(expectedString) + + '\n' + + `Received:\n` + + printReceived(receivedString) + : () => { + const diffString = diff(expectedString, receivedString, { + expand: this.expand, + }); + return ( + this.utils.matcherHint('.toMatchAST') + + '\n\n' + + `Expected AST to equal:\n` + + printExpected(expectedString) + + '\n' + + `Received:\n` + + printReceived(receivedString) + + (diffString ? `\n\nDifference:\n\n${diffString}` : '') + ); + }; + return { + message, + pass, + }; +} + +expect.extend({ + toMatchAST, +}); diff --git a/gateway-js/src/__tests__/queryPlanCucumber.test.ts b/gateway-js/src/__tests__/queryPlanCucumber.test.ts new file mode 100644 index 000000000..ee2725945 --- /dev/null +++ b/gateway-js/src/__tests__/queryPlanCucumber.test.ts @@ -0,0 +1,77 @@ +import gql from 'graphql-tag'; +import { GraphQLSchemaValidationError } from 'apollo-graphql'; +import { defineFeature, loadFeature } from 'jest-cucumber'; +import { DocumentNode } from 'graphql'; + +import { QueryPlan } from '../..'; +import { buildQueryPlan, buildOperationContext, BuildQueryPlanOptions } from '../buildQueryPlan'; +import { getFederatedTestingSchema } from './execution-utils'; + +const buildQueryPlanFeature = loadFeature( + './gateway-js/src/__tests__/build-query-plan.feature' +); + + +const features = [ + buildQueryPlanFeature +]; + +features.forEach((feature) => { + defineFeature(feature, (test) => { + feature.scenarios.forEach((scenario) => { + test(scenario.title, async ({ given, when, then }) => { + let operationDocument: DocumentNode; + let operationString: string; + let queryPlan: QueryPlan; + let options: BuildQueryPlanOptions = { autoFragmentization: false }; + + const { schema, errors, queryPlannerPointer } = getFederatedTestingSchema(); + + if (errors && errors.length > 0) { + throw new GraphQLSchemaValidationError(errors); + } + + const givenQuery = () => { + given(/^query$/im, (operation: string) => { + operationDocument = gql(operation); + operationString = operation; + }) + } + + const whenUsingAutoFragmentization = () => { + when(/using autofragmentization/i, () => { + options = { autoFragmentization: true }; + }) + } + + const thenQueryPlanShouldBe = () => { + then(/^query plan$/i, (expectedQueryPlan: string) => { + queryPlan = buildQueryPlan( + buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + }), + options + ); + + const parsedExpectedPlan = JSON.parse(expectedQueryPlan); + + expect(queryPlan).toEqual(parsedExpectedPlan); + }) + } + + // step over each defined step in the .feature and execute the correct + // matching step fn defined above + scenario.steps.forEach(({ stepText }) => { + const title = stepText.toLocaleLowerCase(); + if (title === "query") givenQuery(); + else if (title === "using autofragmentization") whenUsingAutoFragmentization(); + else if (title === "query plan") thenQueryPlanShouldBe(); + else throw new Error(`Unrecognized steps used in "build-query-plan.feature"`); + }); + }); + }); + }); +}); diff --git a/gateway-js/src/__tests__/testSetup.ts b/gateway-js/src/__tests__/testSetup.ts new file mode 100644 index 000000000..eab33782f --- /dev/null +++ b/gateway-js/src/__tests__/testSetup.ts @@ -0,0 +1,4 @@ +import './matchers/toMatchAST'; +import './matchers/toCallService'; +import './matchers/toHaveBeenCalledBefore'; +import './matchers/toHaveFetched'; diff --git a/gateway-js/src/__tests__/tsconfig.json b/gateway-js/src/__tests__/tsconfig.json new file mode 100644 index 000000000..fcb8707cc --- /dev/null +++ b/gateway-js/src/__tests__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.test.base", + "include": ["**/*"], + "references": [ + { "path": "../../" }, + ] +} diff --git a/gateway-js/src/buildQueryPlan.ts b/gateway-js/src/buildQueryPlan.ts new file mode 100644 index 000000000..764e3becd --- /dev/null +++ b/gateway-js/src/buildQueryPlan.ts @@ -0,0 +1,102 @@ +import { + DocumentNode, + FragmentDefinitionNode, + GraphQLError, + Kind, + OperationDefinitionNode, + print, +} from 'graphql'; +import { + QueryPlan, + OperationContext, + WasmPointer, +} from './QueryPlan'; +import { ComposedGraphQLSchema } from '@apollo/federation'; +import { getQueryPlan } from '@apollo/query-planner-wasm'; + +export interface BuildQueryPlanOptions { + autoFragmentization: boolean; +} + +export function buildQueryPlan( + operationContext: OperationContext, + _options: BuildQueryPlanOptions = { autoFragmentization: false }, +): QueryPlan { + + return getQueryPlan( + operationContext.queryPlannerPointer, + operationContext.operationString, + ); +} + +// Adapted from buildExecutionContext in graphql-js +interface BuildOperationContextOptions { + schema: ComposedGraphQLSchema; + operationDocument: DocumentNode; + operationString: string; + queryPlannerPointer: WasmPointer; + operationName?: string; +}; + +export function buildOperationContext({ + schema, + operationDocument, + operationString, + queryPlannerPointer, + operationName, +}: BuildOperationContextOptions): OperationContext { + let operation: OperationDefinitionNode | undefined; + let operationCount: number = 0; + const fragments: { + [fragmentName: string]: FragmentDefinitionNode; + } = Object.create(null); + operationDocument.definitions.forEach(definition => { + switch (definition.kind) { + case Kind.OPERATION_DEFINITION: + operationCount++; + if (!operationName && operationCount > 1) { + throw new GraphQLError( + 'Must provide operation name if query contains ' + + 'multiple operations.', + ); + } + if ( + !operationName || + (definition.name && definition.name.value === operationName) + ) { + operation = definition; + } + break; + case Kind.FRAGMENT_DEFINITION: + fragments[definition.name.value] = definition; + break; + } + }); + if (!operation) { + if (operationName) { + throw new GraphQLError(`Unknown operation named "${operationName}".`); + } else { + throw new GraphQLError('Must provide an operation.'); + } + } + + // In the case of multiple operations specified (operationName presence validated above), + // `operation` === the operation specified by `operationName` + const trimmedOperationString = operationCount > 1 + ? print({ + kind: Kind.DOCUMENT, + definitions: [ + operation, + ...Object.values(fragments), + ], + }) + : operationString; + + return { + schema, + operation, + fragments, + queryPlannerPointer, + operationString: trimmedOperationString + }; +} diff --git a/gateway-js/src/cache.ts b/gateway-js/src/cache.ts new file mode 100644 index 000000000..5393d9e5e --- /dev/null +++ b/gateway-js/src/cache.ts @@ -0,0 +1,66 @@ +import { CacheManager } from 'make-fetch-happen'; +import { Request, Response, Headers } from 'apollo-server-env'; +import { InMemoryLRUCache } from 'apollo-server-caching'; + +const MAX_SIZE = 5 * 1024 * 1024; // 5MB + +function cacheKey(request: Request) { + return `gateway:request-cache:${request.method}:${request.url}`; +} + +interface CachedRequest { + body: string; + status: number; + statusText: string; + headers: Headers; +} + +export class HttpRequestCache implements CacheManager { + constructor( + public cache: InMemoryLRUCache = new InMemoryLRUCache({ + maxSize: MAX_SIZE, + }), + ) {} + + // Return true if entry exists, else false + async delete(request: Request) { + const key = cacheKey(request); + const entry = await this.cache.get(key); + await this.cache.delete(key); + return Boolean(entry); + } + + async put(request: Request, response: Response) { + // A `HEAD` request has no body to cache and a 304 response could have + // only been negotiated by using a cached body that was still valid. + // Therefore, we do NOT write to the cache in either of these cases. + // Without avoiding this, we will invalidate the cache, thus causing + // subsequent conditional requests (e.g., `If-None-Match: "MD%") to be + // lacking content to conditionally request against and necessitating + // a full request/response. + if (request.method === "HEAD" || response.status === 304) { + return response; + } + + const body = await response.text(); + + this.cache.set(cacheKey(request), { + body, + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + + return new Response(body, response); + } + + async match(request: Request) { + return this.cache.get(cacheKey(request)).then(response => { + if (response) { + const { body, ...requestInit } = response; + return new Response(body, requestInit); + } + return; + }); + } +} diff --git a/gateway-js/src/datasources/LocalGraphQLDataSource.ts b/gateway-js/src/datasources/LocalGraphQLDataSource.ts new file mode 100644 index 000000000..0a532965d --- /dev/null +++ b/gateway-js/src/datasources/LocalGraphQLDataSource.ts @@ -0,0 +1,46 @@ +import { GraphQLRequestContext, GraphQLResponse } from 'apollo-server-types'; +import { + GraphQLSchema, + graphql, + graphqlSync, + DocumentNode, + parse, +} from 'graphql'; +import { + enablePluginsForSchemaResolvers, +} from 'apollo-server-core/dist/utils/schemaInstrumentation'; +import { GraphQLDataSource } from './types'; + +export class LocalGraphQLDataSource = Record> implements GraphQLDataSource { + constructor(public readonly schema: GraphQLSchema) { + enablePluginsForSchemaResolvers(schema); + } + + async process({ + request, + context, + }: Pick, 'request' | 'context'>): Promise< + GraphQLResponse + > { + return graphql({ + schema: this.schema, + source: request.query!, + variableValues: request.variables, + operationName: request.operationName, + contextValue: context, + }); + } + + public sdl(): DocumentNode { + const result = graphqlSync({ + schema: this.schema, + source: `{ _service { sdl }}`, + }); + if (result.errors) { + throw new Error(result.errors.map(error => error.message).join('\n\n')); + } + + const sdl = result.data && result.data._service && result.data._service.sdl; + return parse(sdl); + } +} diff --git a/gateway-js/src/datasources/RemoteGraphQLDataSource.ts b/gateway-js/src/datasources/RemoteGraphQLDataSource.ts new file mode 100644 index 000000000..1461899f9 --- /dev/null +++ b/gateway-js/src/datasources/RemoteGraphQLDataSource.ts @@ -0,0 +1,234 @@ +import { + GraphQLRequestContext, + GraphQLResponse, + ValueOrPromise, + GraphQLRequest, +} from 'apollo-server-types'; +import { + ApolloError, + AuthenticationError, + ForbiddenError, +} from 'apollo-server-errors'; +import { + fetch, + Request, + Headers, + Response, +} from 'apollo-server-env'; +import { isObject } from '../utilities/predicates'; +import { GraphQLDataSource } from './types'; +import createSHA from 'apollo-server-core/dist/utils/createSHA'; + +export class RemoteGraphQLDataSource = Record> implements GraphQLDataSource { + fetcher: typeof fetch = fetch; + + constructor( + config?: Partial> & + object & + ThisType>, + ) { + if (config) { + return Object.assign(this, config); + } + } + + url!: string; + + /** + * Whether the downstream request should be made with automated persisted + * query (APQ) behavior enabled. + * + * @remarks When enabled, the request to the downstream service will first be + * attempted using a SHA-256 hash of the operation rather than including the + * operation itself. If the downstream server supports APQ and has this + * operation registered in its APQ storage, it will be able to complete the + * request without the entirety of the operation document being transmitted. + * + * In the event that the downstream service is unaware of the operation, it + * will respond with an `PersistedQueryNotFound` error and it will be resent + * with the full operation body for fulfillment. + * + * Generally speaking, when the downstream server is processing similar + * operations repeatedly, APQ can offer substantial network savings in terms + * of bytes transmitted over the wire between gateways and downstream servers. + */ + apq: boolean = false; + + async process({ + request, + context, + }: Pick, 'request' | 'context'>): Promise< + GraphQLResponse + > { + // Respect incoming http headers (eg, apollo-federation-include-trace). + const headers = (request.http && request.http.headers) || new Headers(); + headers.set('Content-Type', 'application/json'); + + request.http = { + method: 'POST', + url: this.url, + headers, + }; + + if (this.willSendRequest) { + await this.willSendRequest({ request, context }); + } + + if (!request.query) { + throw new Error("Missing query"); + } + + const apqHash = createSHA('sha256') + .update(request.query) + .digest('hex'); + + const { query, ...requestWithoutQuery } = request; + + const respond = (response: GraphQLResponse, request: GraphQLRequest) => + typeof this.didReceiveResponse === "function" + ? this.didReceiveResponse({ response, request, context }) + : response; + + if (this.apq) { + // Take the original extensions and extend them with + // the necessary "extensions" for APQ handshaking. + requestWithoutQuery.extensions = { + ...request.extensions, + persistedQuery: { + version: 1, + sha256Hash: apqHash, + }, + }; + + const apqOptimisticResponse = + await this.sendRequest(requestWithoutQuery, context); + + // If we didn't receive notice to retry with APQ, then let's + // assume this is the best result we'll get and return it! + if ( + !apqOptimisticResponse.errors || + !apqOptimisticResponse.errors.find(error => + error.message === 'PersistedQueryNotFound') + ) { + return respond(apqOptimisticResponse, requestWithoutQuery); + } + } + + // If APQ was enabled, we'll run the same request again, but add in the + // previously omitted `query`. If APQ was NOT enabled, this is the first + // request (non-APQ, all the way). + const requestWithQuery: GraphQLRequest = { + query, + ...requestWithoutQuery, + }; + const response = await this.sendRequest(requestWithQuery, context); + return respond(response, requestWithQuery); + } + + private async sendRequest( + request: GraphQLRequest, + context: TContext, + ): Promise { + + // This would represent an internal programming error since this shouldn't + // be possible in the way that this method is invoked right now. + if (!request.http) { + throw new Error("Internal error: Only 'http' requests are supported.") + } + + // We don't want to serialize the `http` properties into the body that is + // being transmitted. Instead, we want those to be used to indicate what + // we're accessing (e.g. url) and what we access it with (e.g. headers). + const { http, ...requestWithoutHttp } = request; + const fetchRequest = new Request(http.url, { + ...http, + body: JSON.stringify(requestWithoutHttp), + }); + + let fetchResponse: Response | undefined; + + try { + // Use our local `fetcher` to allow for fetch injection + fetchResponse = await this.fetcher(fetchRequest); + + if (!fetchResponse.ok) { + throw await this.errorFromResponse(fetchResponse); + } + + const body = await this.parseBody(fetchResponse, fetchRequest, context); + + if (!isObject(body)) { + throw new Error(`Expected JSON response body, but received: ${body}`); + } + + return { + ...body, + http: fetchResponse, + }; + } catch (error) { + this.didEncounterError(error, fetchRequest, fetchResponse); + throw error; + } + } + + public willSendRequest?( + requestContext: Pick< + GraphQLRequestContext, + 'request' | 'context' + >, + ): ValueOrPromise; + + public didReceiveResponse?( + requestContext: Required, + 'request' | 'response' | 'context'> + >, + ): ValueOrPromise; + + public didEncounterError( + error: Error, + _fetchRequest: Request, + _fetchResponse?: Response + ) { + throw error; + } + + public parseBody( + fetchResponse: Response, + _fetchRequest?: Request, + _context?: TContext, + ): Promise { + const contentType = fetchResponse.headers.get('Content-Type'); + if (contentType && contentType.startsWith('application/json')) { + return fetchResponse.json(); + } else { + return fetchResponse.text(); + } + } + + public async errorFromResponse(response: Response) { + const message = `${response.status}: ${response.statusText}`; + + let error: ApolloError; + if (response.status === 401) { + error = new AuthenticationError(message); + } else if (response.status === 403) { + error = new ForbiddenError(message); + } else { + error = new ApolloError(message); + } + + const body = await this.parseBody(response); + + Object.assign(error.extensions, { + response: { + url: response.url, + status: response.status, + statusText: response.statusText, + body, + }, + }); + + return error; + } +} diff --git a/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts new file mode 100644 index 000000000..9d710256f --- /dev/null +++ b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts @@ -0,0 +1,44 @@ +import { LocalGraphQLDataSource } from '../LocalGraphQLDataSource'; +import { buildFederatedSchema } from '@apollo/federation'; +import gql from 'graphql-tag'; + +describe('constructing requests', () => { + it('accepts context', async () => { + const typeDefs = gql` + type Query { + me: User + } + type User { + id: ID + name: String! + } + `; + const resolvers = { + Query: { + me(_, __, { userId }) { + const users = [ + { id: 1, name: 'otherGuy' }, + { id: 2, name: 'james' }, + { + id: 3, + name: 'someoneElse', + }, + ]; + return users.find(user => user.id === userId); + }, + }, + }; + const schema = buildFederatedSchema([{ typeDefs, resolvers }]); + + const DataSource = new LocalGraphQLDataSource(schema); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + }, + context: { userId: 2 }, + }); + + expect(data).toEqual({ me: { name: 'james' } }); + }); +}); diff --git a/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts new file mode 100644 index 000000000..c2b0e45b2 --- /dev/null +++ b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts @@ -0,0 +1,543 @@ +import { fetch } from '__mocks__/apollo-server-env'; + +import { + ApolloError, + AuthenticationError, + ForbiddenError, +} from 'apollo-server-errors'; + +import { RemoteGraphQLDataSource } from '../RemoteGraphQLDataSource'; +import { Headers } from 'apollo-server-env'; +import { GraphQLRequestContext } from 'apollo-server-types'; + +beforeEach(() => { + fetch.mockReset(); +}); + +describe('constructing requests', () => { + describe('without APQ', () => { + it('stringifies a request with a query', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: false, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { query: '{ me { name } }' }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(1); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { query: '{ me { name } }' }, + }); + }); + + it('passes variables', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: false, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(1); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { query: '{ me { name } }', variables: { id: '1' } }, + }); + }); + }); + + describe('with APQ', () => { + // When changing this, adjust the SHA-256 hash below as well. + const query = '{ me { name } }'; + + // This is a SHA-256 hash of `query` above. + const sha256Hash = + "b8d9506e34c83b0e53c2aa463624fcea354713bc38f95276e6f0bd893ffb5b88"; + + describe('miss', () => { + const apqNotFoundResponse = { + "errors": [ + { + "message": "PersistedQueryNotFound", + "extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND", + "exception": { + "stacktrace": ["PersistedQueryNotFoundError: PersistedQueryNotFound"] + } + } + } + ] + }; + + it('stringifies a request with a query', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: true, + }); + + fetch.mockJSONResponseOnce(apqNotFoundResponse); + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { query }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(2); + expect(fetch).toHaveFetchedNth(1, { + url: 'https://api.example.com/foo', + body: { + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + expect(fetch).toHaveFetchedNth(2, { + url: 'https://api.example.com/foo', + body: { + query, + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + }); + + it('passes variables', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: true, + }); + + fetch.mockJSONResponseOnce(apqNotFoundResponse); + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { + query, + variables: { id: '1' }, + }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(2); + expect(fetch).toHaveFetchedNth(1, { + url: 'https://api.example.com/foo', + body: { + variables: { id: '1' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + + expect(fetch).toHaveFetchedNth(2, { + url: 'https://api.example.com/foo', + body: { + query, + variables: { id: '1' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + }); + }); + + describe('hit', () => { + it('stringifies a request with a query', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: true, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { query }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(1); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + }); + + it('passes variables', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + apq: true, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { + query, + variables: { id: '1' }, + }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toBeCalledTimes(1); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { + variables: { id: '1' }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash, + } + } + }, + }); + }); + }); + }); +}); + +describe('fetcher', () => { + it('uses a custom provided `fetcher`', async () => { + const injectedFetch = fetch.mockJSONResponseOnce({ data: { injected: true } }); + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + fetcher: injectedFetch, + }); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(injectedFetch).toHaveBeenCalled(); + expect(data).toEqual({injected: true}); + + }); + +}); + +describe('willSendRequest', () => { + it('allows for modifying variables', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + willSendRequest: ({ request }) => { + request.variables = JSON.stringify(request.variables); + }, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { + query: '{ me { name } }', + variables: JSON.stringify({ id: '1' }), + }, + }); + }); + + it('accepts context', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + willSendRequest: ({ request, context }) => { + request.http.headers.set('x-user-id', context.userId); + }, + }); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const { data } = await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: { userId: '1234' }, + }); + + expect(data).toEqual({ me: 'james' }); + expect(fetch).toHaveFetched({ + url: 'https://api.example.com/foo', + body: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + headers: { + 'x-user-id': '1234', + }, + }); + }); +}); + +describe('didReceiveResponse', () => { + it('can accept and modify context', async () => { + interface MyContext { + surrogateKeys: string[]; + } + + class MyDataSource extends RemoteGraphQLDataSource { + url = 'https://api.example.com/foo'; + + didReceiveResponse({ + request, + response, + }: Required, + 'request' | 'response' | 'context' + >>) { + const surrogateKeys = + request.http && request.http.headers.get('surrogate-keys'); + if (surrogateKeys) { + context.surrogateKeys.push(...surrogateKeys.split(' ')); + } + return response; + } + } + + const DataSource = new MyDataSource(); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + const context: MyContext = { surrogateKeys: [] }; + await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + http: { + method: 'GET', + url: 'https://api.example.com/foo', + headers: new Headers({ 'Surrogate-Keys': 'abc def' }), + }, + }, + context, + }); + + expect(context).toEqual({ surrogateKeys: ['abc', 'def'] }); + }); + + it('is only called once', async () => { + class MyDataSource extends RemoteGraphQLDataSource { + url = 'https://api.example.com/foo'; + + didReceiveResponse({ + response, + }: Required, + 'request' | 'response' | 'context' + >>) { + return response; + } + } + + const DataSource = new MyDataSource(); + const spyDidReceiveResponse = + jest.spyOn(DataSource, 'didReceiveResponse'); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1); + + }); + + // APQ makes two requests, so make sure only one calls the response hook. + it('is only called once when apq is enabled', async () => { + class MyDataSource extends RemoteGraphQLDataSource { + url = 'https://api.example.com/foo'; + apq = true; + + didReceiveResponse({ + response, + }: Required, + 'request' | 'response' | 'context' + >>) { + return response; + } + } + + const DataSource = new MyDataSource(); + const spyDidReceiveResponse = jest.spyOn(DataSource, 'didReceiveResponse'); + + fetch.mockJSONResponseOnce({ data: { me: 'james' } }); + + await DataSource.process({ + request: { + query: '{ me { name } }', + variables: { id: '1' }, + }, + context: {}, + }); + + expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1); + + }); +}); + +describe('error handling', () => { + it('throws an AuthenticationError when the response status is 401', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + }); + + fetch.mockResponseOnce('Invalid token', undefined, 401); + + const result = DataSource.process({ + request: { query: '{ me { name } }' }, + context: {}, + }); + await expect(result).rejects.toThrow(AuthenticationError); + await expect(result).rejects.toMatchObject({ + extensions: { + code: 'UNAUTHENTICATED', + response: { + status: 401, + body: 'Invalid token', + }, + }, + }); + }); + + it('throws a ForbiddenError when the response status is 403', async () => { + const DataSource = new RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + }); + + fetch.mockResponseOnce('No access', undefined, 403); + + const result = DataSource.process({ + request: { query: '{ me { name } }' }, + context: {}, + }); + 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 RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + }); + + fetch.mockResponseOnce('Oops', undefined, 500); + + const result = DataSource.process({ + request: { query: '{ me { name } }' }, + context: {}, + }); + await expect(result).rejects.toThrow(ApolloError); + 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 RemoteGraphQLDataSource({ + url: 'https://api.example.com/foo', + }); + + fetch.mockResponseOnce( + JSON.stringify({ + errors: [ + { + message: 'Houston, we have a problem.', + }, + ], + }), + { 'Content-Type': 'application/json' }, + 500, + ); + + const result = DataSource.process({ + request: { query: '{ me { name } }' }, + context: {}, + }); + await expect(result).rejects.toThrow(ApolloError); + await expect(result).rejects.toMatchObject({ + extensions: { + response: { + status: 500, + body: { + errors: [ + { + message: 'Houston, we have a problem.', + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/gateway-js/src/datasources/__tests__/tsconfig.json b/gateway-js/src/datasources/__tests__/tsconfig.json new file mode 100644 index 000000000..c1e0db227 --- /dev/null +++ b/gateway-js/src/datasources/__tests__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [ + { "path": "../../../" }, + ] +} diff --git a/gateway-js/src/datasources/index.ts b/gateway-js/src/datasources/index.ts new file mode 100644 index 000000000..6ac3ac7ab --- /dev/null +++ b/gateway-js/src/datasources/index.ts @@ -0,0 +1,3 @@ +export { LocalGraphQLDataSource } from './LocalGraphQLDataSource'; +export { RemoteGraphQLDataSource } from './RemoteGraphQLDataSource'; +export { GraphQLDataSource } from './types'; diff --git a/gateway-js/src/datasources/types.ts b/gateway-js/src/datasources/types.ts new file mode 100644 index 000000000..c112c3096 --- /dev/null +++ b/gateway-js/src/datasources/types.ts @@ -0,0 +1,7 @@ +import { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types'; + +export interface GraphQLDataSource = Record> { + process( + request: Pick, 'request' | 'context'>, + ): Promise; +} diff --git a/gateway-js/src/executeQueryPlan.ts b/gateway-js/src/executeQueryPlan.ts new file mode 100644 index 000000000..f9f1ef556 --- /dev/null +++ b/gateway-js/src/executeQueryPlan.ts @@ -0,0 +1,527 @@ +import { + GraphQLExecutionResult, + GraphQLRequestContext, +} from 'apollo-server-types'; +import { Headers } from 'apollo-server-env'; +import { + execute, + GraphQLError, + Kind, + TypeNameMetaFieldDef, + GraphQLFieldResolver, +} from 'graphql'; +import { Trace, google } from 'apollo-engine-reporting-protobuf'; +import { defaultRootOperationNameLookup } from '@apollo/federation'; +import { GraphQLDataSource } from './datasources/types'; +import { + FetchNode, + PlanNode, + QueryPlan, + ResponsePath, + OperationContext, + QueryPlanSelectionNode, + QueryPlanFieldNode, + getResponseName +} from './QueryPlan'; +import { deepMerge } from './utilities/deepMerge'; + +export type ServiceMap = { + [serviceName: string]: GraphQLDataSource; +}; + +type ResultMap = Record; + +interface ExecutionContext { + queryPlan: QueryPlan; + operationContext: OperationContext; + serviceMap: ServiceMap; + requestContext: GraphQLRequestContext; + errors: GraphQLError[]; +} + +export async function executeQueryPlan( + queryPlan: QueryPlan, + serviceMap: ServiceMap, + requestContext: GraphQLRequestContext, + operationContext: OperationContext, +): Promise { + const errors: GraphQLError[] = []; + + const context: ExecutionContext = { + queryPlan, + operationContext, + serviceMap, + requestContext, + errors, + }; + + let data: ResultMap | undefined | null = Object.create(null); + + const captureTraces = !!( + requestContext.metrics && requestContext.metrics.captureTraces + ); + + if (queryPlan.node) { + const traceNode = await executeNode( + context, + queryPlan.node, + data!, + [], + captureTraces, + ); + if (captureTraces) { + requestContext.metrics!.queryPlanTrace = traceNode; + } + } + + // FIXME: Re-executing the query is a pretty heavy handed way of making sure + // only explicitly requested fields are included and field ordering follows + // the original query. + // It is also used to allow execution of introspection queries though. + try { + ({ data } = await execute({ + schema: operationContext.schema, + document: { + kind: Kind.DOCUMENT, + definitions: [ + operationContext.operation, + ...Object.values(operationContext.fragments), + ], + }, + rootValue: data, + variableValues: requestContext.request.variables, + // See also `wrapSchemaWithAliasResolver` in `gateway-js/src/index.ts`. + fieldResolver: defaultFieldResolverWithAliasSupport, + })); + } catch (error) { + return { errors: [error] }; + } + + return errors.length === 0 ? { data } : { errors, data }; +} + +// Note: this function always returns a protobuf QueryPlanNode tree, even if +// we're going to ignore it, because it makes the code much simpler and more +// typesafe. However, it doesn't actually ask for traces from the backend +// service unless we are capturing traces for Engine. +async function executeNode( + context: ExecutionContext, + node: PlanNode, + results: ResultMap | ResultMap[], + path: ResponsePath, + captureTraces: boolean, +): Promise { + if (!results) { + // XXX I don't understand `results` threading well enough to understand when this happens + // and if this corresponds to a real query plan node that should be reported or not. + // + // This may be if running something like `query { fooOrNullFromServiceA { + // somethingFromServiceB } }` and the first field is null, then we don't bother to run the + // inner field at all. + return new Trace.QueryPlanNode(); + } + + switch (node.kind) { + case 'Sequence': { + const traceNode = new Trace.QueryPlanNode.SequenceNode(); + for (const childNode of node.nodes) { + const childTraceNode = await executeNode( + context, + childNode, + results, + path, + captureTraces, + ); + traceNode.nodes.push(childTraceNode!); + } + return new Trace.QueryPlanNode({ sequence: traceNode }); + } + case 'Parallel': { + const childTraceNodes = await Promise.all( + node.nodes.map(async childNode => + executeNode(context, childNode, results, path, captureTraces), + ), + ); + return new Trace.QueryPlanNode({ + parallel: new Trace.QueryPlanNode.ParallelNode({ + nodes: childTraceNodes, + }), + }); + } + case 'Flatten': { + return new Trace.QueryPlanNode({ + flatten: new Trace.QueryPlanNode.FlattenNode({ + responsePath: node.path.map( + id => + new Trace.QueryPlanNode.ResponsePathElement( + typeof id === 'string' ? { fieldName: id } : { index: id }, + ), + ), + node: await executeNode( + context, + node.node, + flattenResultsAtPath(results, node.path), + [...path, ...node.path], + captureTraces, + ), + }), + }); + } + case 'Fetch': { + const traceNode = new Trace.QueryPlanNode.FetchNode({ + serviceName: node.serviceName, + // executeFetch will fill in the other fields if desired. + }); + try { + await executeFetch( + context, + node, + results, + path, + captureTraces ? traceNode : null, + ); + } catch (error) { + context.errors.push(error); + } + return new Trace.QueryPlanNode({ fetch: traceNode }); + } + } +} + +async function executeFetch( + context: ExecutionContext, + fetch: FetchNode, + results: ResultMap | ResultMap[], + _path: ResponsePath, + traceNode: Trace.QueryPlanNode.FetchNode | null, +): Promise { + const logger = context.requestContext.logger || console; + const service = context.serviceMap[fetch.serviceName]; + if (!service) { + throw new Error(`Couldn't find service with name "${fetch.serviceName}"`); + } + + const entities = Array.isArray(results) ? results : [results]; + if (entities.length < 1) return; + + let variables = Object.create(null); + if (fetch.variableUsages) { + for (const variableName of fetch.variableUsages) { + const providedVariables = context.requestContext.request.variables; + if ( + providedVariables && + typeof providedVariables[variableName] !== 'undefined' + ) { + variables[variableName] = providedVariables[variableName]; + } + } + } + + if (!fetch.requires) { + const dataReceivedFromService = await sendOperation( + context, + fetch.operation, + variables, + ); + + for (const entity of entities) { + deepMerge(entity, dataReceivedFromService); + } + } else { + const requires = fetch.requires; + + const representations: ResultMap[] = []; + const representationToEntity: number[] = []; + + entities.forEach((entity, index) => { + const representation = executeSelectionSet(entity, requires); + if (representation && representation[TypeNameMetaFieldDef.name]) { + representations.push(representation); + representationToEntity.push(index); + } + }); + + if ('representations' in variables) { + throw new Error(`Variables cannot contain key "representations"`); + } + + const dataReceivedFromService = await sendOperation( + context, + fetch.operation, + { ...variables, representations }, + ); + + if (!dataReceivedFromService) { + return; + } + + if ( + !( + dataReceivedFromService._entities && + Array.isArray(dataReceivedFromService._entities) + ) + ) { + throw new Error(`Expected "data._entities" in response to be an array`); + } + + const receivedEntities = dataReceivedFromService._entities; + + if (receivedEntities.length !== representations.length) { + throw new Error( + `Expected "data._entities" to contain ${representations.length} elements`, + ); + } + + for (let i = 0; i < entities.length; i++) { + deepMerge(entities[representationToEntity[i]], receivedEntities[i]); + } + } + + async function sendOperation( + context: ExecutionContext, + source: string, + variables: Record, + ): Promise { + // We declare this as 'any' because it is missing url and method, which + // GraphQLRequest.http is supposed to have if it exists. + let http: any; + + // If we're capturing a trace for Engine, then save the operation text to + // the node we're building and tell the federated service to include a trace + // in its response. + if (traceNode) { + http = { + headers: new Headers({ 'apollo-federation-include-trace': 'ftv1' }), + }; + if ( + context.requestContext.metrics && + context.requestContext.metrics.startHrTime + ) { + traceNode.sentTimeOffset = durationHrTimeToNanos( + process.hrtime(context.requestContext.metrics.startHrTime), + ); + } + traceNode.sentTime = dateToProtoTimestamp(new Date()); + } + + const response = await service.process({ + request: { + query: source, + variables, + http, + }, + context: context.requestContext.context, + }); + + if (response.errors) { + const errors = response.errors.map(error => + downstreamServiceError( + error.message, + fetch.serviceName, + source, + variables, + error.extensions, + error.path, + ), + ); + context.errors.push(...errors); + } + + // If we're capturing a trace for Engine, save the received trace into the + // query plan. + if (traceNode) { + traceNode.receivedTime = dateToProtoTimestamp(new Date()); + + if (response.extensions && response.extensions.ftv1) { + const traceBase64 = response.extensions.ftv1; + + let traceBuffer: Buffer | undefined; + let traceParsingFailed = false; + try { + // XXX support non-Node implementations by using Uint8Array? protobufjs + // supports that, but there's not a no-deps base64 implementation. + traceBuffer = Buffer.from(traceBase64, 'base64'); + } catch (err) { + logger.error( + `error decoding base64 for federated trace from ${fetch.serviceName}: ${err}`, + ); + traceParsingFailed = true; + } + + if (traceBuffer) { + try { + const trace = Trace.decode(traceBuffer); + traceNode.trace = trace; + } catch (err) { + logger.error( + `error decoding protobuf for federated trace from ${fetch.serviceName}: ${err}`, + ); + traceParsingFailed = true; + } + } + if (traceNode.trace) { + // Federation requires the root operations in the composed schema + // to have the default names (Query, Mutation, Subscription) even + // if the implementing services choose different names, so we override + // whatever the implementing service reported here. + const rootTypeName = + defaultRootOperationNameLookup[ + context.operationContext.operation.operation + ]; + traceNode.trace.root?.child?.forEach((child) => { + child.parentType = rootTypeName; + }); + } + traceNode.traceParsingFailed = traceParsingFailed; + } + } + + return response.data; + } +} + +/** + * + * @param source Result of GraphQL execution. + * @param selectionSet + */ +function executeSelectionSet( + source: Record | null, + selections: QueryPlanSelectionNode[], +): Record | null { + + // If the underlying service has returned null for the parent (source) + // then there is no need to iterate through the parent's selection set + if (source === null) { + return null; + } + + const result: Record = Object.create(null); + + for (const selection of selections) { + switch (selection.kind) { + case Kind.FIELD: + const responseName = getResponseName(selection as QueryPlanFieldNode); + const selections = (selection as QueryPlanFieldNode).selections; + + if (typeof source[responseName] === 'undefined') { + throw new Error(`Field "${responseName}" was not found in response.`); + } + if (Array.isArray(source[responseName])) { + result[responseName] = source[responseName].map((value: any) => + selections ? executeSelectionSet(value, selections) : value, + ); + } else if (selections) { + result[responseName] = executeSelectionSet( + source[responseName], + selections, + ); + } else { + result[responseName] = source[responseName]; + } + break; + case Kind.INLINE_FRAGMENT: + if (!selection.typeCondition) continue; + + const typename = source && source['__typename']; + if (!typename) continue; + + if (typename === selection.typeCondition) { + deepMerge( + result, + executeSelectionSet(source, selection.selections), + ); + } + break; + } + } + + return result; +} + +function flattenResultsAtPath(value: any, path: ResponsePath): any { + if (path.length === 0) return value; + if (value === undefined || value === null) return value; + + const [current, ...rest] = path; + if (current === '@') { + return value.flatMap((element: any) => flattenResultsAtPath(element, rest)); + } else { + return flattenResultsAtPath(value[current], rest); + } +} + +function downstreamServiceError( + message: string | undefined, + serviceName: string, + query: string, + variables?: Record, + extensions?: Record, + path?: ReadonlyArray | undefined, +) { + if (!message) { + message = `Error while fetching subquery from service "${serviceName}"`; + } + extensions = { + code: 'DOWNSTREAM_SERVICE_ERROR', + // XXX The presence of a serviceName in extensions is used to + // determine if this error should be captured for metrics reporting. + serviceName, + query, + variables, + ...extensions, + }; + return new GraphQLError( + message, + undefined, + undefined, + undefined, + path, + undefined, + extensions, + ); +} + +export const defaultFieldResolverWithAliasSupport: GraphQLFieldResolver< + any, + any +> = function(source, args, contextValue, info) { + // ensure source is a value for which property access is acceptable. + if (typeof source === 'object' || typeof source === 'function') { + // if this is an alias, check it first because a downstream service + // would have returned the data *already cast* to an alias responseName + const property = source[info.path.key]; + if (typeof property === 'function') { + return source[info.fieldName](args, contextValue, info); + } + return property; + } +}; + +// Converts an hrtime array (as returned from process.hrtime) to nanoseconds. +// +// ONLY CALL THIS ON VALUES REPRESENTING DELTAS, NOT ON THE RAW RETURN VALUE +// FROM process.hrtime() WITH NO ARGUMENTS. +// +// The entire point of the hrtime data structure is that the JavaScript Number +// type can't represent all int64 values without loss of precision: +// Number.MAX_SAFE_INTEGER nanoseconds is about 104 days. Calling this function +// on a duration that represents a value less than 104 days is fine. Calling +// this function on an absolute time (which is generally roughly time since +// system boot) is not a good idea. +// +// XXX We should probably use google.protobuf.Duration on the wire instead of +// ever trying to store durations in a single number. +function durationHrTimeToNanos(hrtime: [number, number]) { + return hrtime[0] * 1e9 + hrtime[1]; +} + +// Converts a JS Date into a Timestamp. +function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp { + const totalMillis = +date; + const millis = totalMillis % 1000; + return new google.protobuf.Timestamp({ + seconds: (totalMillis - millis) / 1000, + nanos: millis * 1e6, + }); +} diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts new file mode 100644 index 000000000..128803d28 --- /dev/null +++ b/gateway-js/src/index.ts @@ -0,0 +1,837 @@ +import { + GraphQLService, + SchemaChangeCallback, + Unsubscriber, + GraphQLServiceEngineConfig, +} from 'apollo-server-core'; +import { + GraphQLExecutionResult, + Logger, + GraphQLRequestContextExecutionDidStart, +} from 'apollo-server-types'; +import { InMemoryLRUCache } from 'apollo-server-caching'; +import { + isObjectType, + isIntrospectionType, + GraphQLSchema, + GraphQLError, + VariableDefinitionNode, +} from 'graphql'; +import { GraphQLSchemaValidationError } from 'apollo-graphql'; +import { composeAndValidate, ServiceDefinition, ComposedGraphQLSchema } from '@apollo/federation'; +import loglevel from 'loglevel'; + +import { buildQueryPlan, buildOperationContext } from './buildQueryPlan'; +import { + executeQueryPlan, + ServiceMap, + defaultFieldResolverWithAliasSupport, +} from './executeQueryPlan'; + +import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint'; +import { + getServiceDefinitionsFromStorage, + CompositionMetadata, +} from './loadServicesFromStorage'; + +import { serializeQueryPlan, QueryPlan, OperationContext, WasmPointer } from './QueryPlan'; +import { GraphQLDataSource } from './datasources/types'; +import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource'; +import { HeadersInit } from 'node-fetch'; +import { getVariableValues } from 'graphql/execution/values'; +import fetcher from 'make-fetch-happen'; +import { HttpRequestCache } from './cache'; +import { fetch } from 'apollo-server-env'; +import { getQueryPlanner } from '@apollo/query-planner-wasm'; + +export type ServiceEndpointDefinition = Pick; + +interface GatewayConfigBase { + debug?: boolean; + logger?: Logger; + // TODO: expose the query plan in a more flexible JSON format in the future + // and remove this config option in favor of `exposeQueryPlan`. Playground + // should cutover to use the new option when it's built. + __exposeQueryPlanExperimental?: boolean; + buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; + + // experimental observability callbacks + experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; + experimental_didFailComposition?: Experimental_DidFailCompositionCallback; + experimental_updateServiceDefinitions?: Experimental_UpdateServiceDefinitions; + experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; + experimental_pollInterval?: number; + experimental_approximateQueryPlanStoreMiB?: number; + experimental_autoFragmentization?: boolean; + fetcher?: typeof fetch; + serviceHealthCheck?: boolean; +} + +interface RemoteGatewayConfig extends GatewayConfigBase { + serviceList: ServiceEndpointDefinition[]; + introspectionHeaders?: HeadersInit; +} + +interface ManagedGatewayConfig extends GatewayConfigBase { + federationVersion?: number; +} +interface LocalGatewayConfig extends GatewayConfigBase { + localServiceList: ServiceDefinition[]; +} + +export type GatewayConfig = + | RemoteGatewayConfig + | LocalGatewayConfig + | ManagedGatewayConfig; + +type DataSourceMap = { + [serviceName: string]: { url?: string; dataSource: GraphQLDataSource }; +}; + +function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { + return 'localServiceList' in config; +} + +function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { + return 'serviceList' in config; +} + +function isManagedConfig( + config: GatewayConfig, +): config is ManagedGatewayConfig { + return !isRemoteConfig(config) && !isLocalConfig(config); +} + +export type Experimental_DidResolveQueryPlanCallback = ({ + queryPlan, + serviceMap, + operationContext, + requestContext, +}: { + readonly queryPlan: QueryPlan; + readonly serviceMap: ServiceMap; + readonly operationContext: OperationContext; + readonly requestContext: GraphQLRequestContextExecutionDidStart>; +}) => void; + +export type Experimental_DidFailCompositionCallback = ({ + errors, + serviceList, + compositionMetadata, +}: { + readonly errors: GraphQLError[]; + readonly serviceList: ServiceDefinition[]; + readonly compositionMetadata?: CompositionMetadata; +}) => void; + +export interface Experimental_CompositionInfo { + serviceDefinitions: ServiceDefinition[]; + schema: GraphQLSchema; + compositionMetadata?: CompositionMetadata; +} + +export type Experimental_DidUpdateCompositionCallback = ( + currentConfig: Experimental_CompositionInfo, + previousConfig?: Experimental_CompositionInfo, +) => void; + +/** + * **Note:** It's possible for a schema to be the same (`isNewSchema: false`) when + * `serviceDefinitions` have changed. For example, during type migration, the + * composed schema may be identical but the `serviceDefinitions` would differ + * since a type has moved from one service to another. + */ +export type Experimental_UpdateServiceDefinitions = ( + config: GatewayConfig, +) => Promise<{ + serviceDefinitions?: ServiceDefinition[]; + compositionMetadata?: CompositionMetadata; + isNewSchema: boolean; +}>; + +type Await = T extends Promise ? U : T; + +// Local state to track whether particular UX-improving warning messages have +// already been emitted. This is particularly useful to prevent recurring +// warnings of the same type in, e.g. repeating timers, which don't provide +// additional value when they are repeated over and over during the life-time +// of a server. +type WarnedStates = { + remoteWithLocalConfig?: boolean; +}; + +export const GCS_RETRY_COUNT = 5; + +export function getDefaultGcsFetcher() { + return fetcher.defaults({ + cacheManager: new HttpRequestCache(), + // All headers should be lower-cased here, as `make-fetch-happen` + // treats differently cased headers as unique (unlike the `Headers` object). + // @see: https://git.io/JvRUa + headers: { + 'user-agent': `apollo-gateway/${require('../package.json').version}`, + }, + retry: { + retries: GCS_RETRY_COUNT, + // The default factor: expected attempts at 0, 1, 3, 7, 15, and 31 seconds elapsed + factor: 2, + // 1 second + minTimeout: 1000, + randomize: true, + }, + }); +} + +export const HEALTH_CHECK_QUERY = + 'query __ApolloServiceHealthCheck__ { __typename }'; +export const SERVICE_DEFINITION_QUERY = + 'query __ApolloGetServiceDefinition__ { _service { sdl } }'; + +export class ApolloGateway implements GraphQLService { + public schema?: ComposedGraphQLSchema; + protected serviceMap: DataSourceMap = Object.create(null); + protected config: GatewayConfig; + private logger: Logger; + protected queryPlanStore?: InMemoryLRUCache; + private engineConfig: GraphQLServiceEngineConfig | undefined; + private pollingTimer?: NodeJS.Timer; + private onSchemaChangeListeners = new Set(); + private serviceDefinitions: ServiceDefinition[] = []; + private compositionMetadata?: CompositionMetadata; + private serviceSdlCache = new Map(); + private warnedStates: WarnedStates = Object.create(null); + private queryPlannerPointer?: WasmPointer; + + private fetcher: typeof fetch = getDefaultGcsFetcher(); + + // Observe query plan, service info, and operation info prior to execution. + // The information made available here will give insight into the resulting + // query plan and the inputs that generated it. + protected experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback; + // Observe composition failures and the ServiceList that caused them. This + // enables reporting any issues that occur during composition. Implementors + // will be interested in addressing these immediately. + protected experimental_didFailComposition?: Experimental_DidFailCompositionCallback; + // Used to communicated composition changes, and what definitions caused + // those updates + protected experimental_didUpdateComposition?: Experimental_DidUpdateCompositionCallback; + // Used for overriding the default service list fetcher. This should return + // an array of ServiceDefinition. *This function must be awaited.* + protected updateServiceDefinitions: Experimental_UpdateServiceDefinitions; + // how often service defs should be loaded/updated (in ms) + protected experimental_pollInterval?: number; + + private experimental_approximateQueryPlanStoreMiB?: number; + + constructor(config?: GatewayConfig) { + this.config = { + // TODO: expose the query plan in a more flexible JSON format in the future + // and remove this config option in favor of `exposeQueryPlan`. Playground + // should cutover to use the new option when it's built. + __exposeQueryPlanExperimental: process.env.NODE_ENV !== 'production', + ...config, + }; + + // Setup logging facilities + if (this.config.logger) { + this.logger = this.config.logger; + } else { + // If the user didn't provide their own logger, we'll initialize one. + const loglevelLogger = loglevel.getLogger(`apollo-gateway`); + + // And also support the `debug` option, if it's truthy. + if (this.config.debug === true) { + loglevelLogger.setLevel(loglevelLogger.levels.DEBUG); + } else { + loglevelLogger.setLevel(loglevelLogger.levels.WARN); + } + + this.logger = loglevelLogger; + } + + if (isLocalConfig(this.config)) { + const { schema, composedSdl } = this.createSchema(this.config.localServiceList); + this.schema = schema; + + if (!composedSdl) { + this.logger.error("A valid schema couldn't be composed.") + } else { + this.queryPlannerPointer = getQueryPlanner(composedSdl); + } + } + + this.initializeQueryPlanStore(); + + // this will be overwritten if the config provides experimental_updateServiceDefinitions + this.updateServiceDefinitions = this.loadServiceDefinitions; + + if (config) { + this.updateServiceDefinitions = + config.experimental_updateServiceDefinitions || + this.updateServiceDefinitions; + // set up experimental observability callbacks + this.experimental_didResolveQueryPlan = + config.experimental_didResolveQueryPlan; + this.experimental_didFailComposition = + config.experimental_didFailComposition; + this.experimental_didUpdateComposition = + config.experimental_didUpdateComposition; + + this.experimental_approximateQueryPlanStoreMiB = + config.experimental_approximateQueryPlanStoreMiB; + + if ( + isManagedConfig(config) && + config.experimental_pollInterval && + config.experimental_pollInterval < 10000 + ) { + this.experimental_pollInterval = 10000; + this.logger.warn( + 'Polling Apollo services at a frequency of less than once per 10 seconds (10000) is disallowed. Instead, the minimum allowed pollInterval of 10000 will be used. Please reconfigure your experimental_pollInterval accordingly. If this is problematic for your team, please contact support.', + ); + } else { + this.experimental_pollInterval = config.experimental_pollInterval; + } + + // Warn against using the pollInterval and a serviceList simultaneously + if (config.experimental_pollInterval && isRemoteConfig(config)) { + this.logger.warn( + 'Polling running services is dangerous and not recommended in production. ' + + 'Polling should only be used against a registry. ' + + 'If you are polling running services, use with caution.', + ); + } + + if (config.fetcher) { + this.fetcher = config.fetcher; + } + } + } + + public async load(options?: { engine?: GraphQLServiceEngineConfig }) { + if (options && options.engine) { + if (!options.engine.graphVariant) + this.logger.warn('No graph variant provided. Defaulting to `current`.'); + this.engineConfig = options.engine; + } + + await this.updateComposition(); + if ( + (isManagedConfig(this.config) || this.experimental_pollInterval) && + !this.pollingTimer + ) { + this.pollServices(); + } + + const { graphId, graphVariant } = (options && options.engine) || {}; + const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; + + this.logger.info( + `Gateway successfully loaded schema.\n\t* Mode: ${mode}${ + graphId ? `\n\t* Service: ${graphId}@${graphVariant || 'current'}` : '' + }`, + ); + + return { + // we know this will be here since we're awaiting this.updateComposition + // before here which sets this.schema + schema: this.schema!, + executor: this.executor, + }; + } + + protected async updateComposition(): Promise { + let result: Await>; + this.logger.debug('Checking service definitions...'); + try { + result = await this.updateServiceDefinitions(this.config); + } catch (e) { + this.logger.error( + "Error checking for changes to service definitions: " + + (e && e.message || e) + ); + throw e; + } + + if ( + !result.serviceDefinitions || + JSON.stringify(this.serviceDefinitions) === + JSON.stringify(result.serviceDefinitions) + ) { + this.logger.debug('No change in service definitions since last check.'); + return; + } + + const previousSchema = this.schema; + const previousServiceDefinitions = this.serviceDefinitions; + const previousCompositionMetadata = this.compositionMetadata; + + if (previousSchema) { + this.logger.info("New service definitions were found."); + } + + // Run service health checks before we commit and update the new schema. + // This is the last chance to bail out of a schema update. + if (this.config.serviceHealthCheck) { + // Here we need to construct new datasources based on the new schema info + // so we can check the health of the services we're _updating to_. + const serviceMap = result.serviceDefinitions.reduce( + (serviceMap, serviceDef) => { + serviceMap[serviceDef.name] = { + url: serviceDef.url, + dataSource: this.createDataSource(serviceDef), + }; + return serviceMap; + }, + Object.create(null) as DataSourceMap, + ); + + try { + await this.serviceHealthCheck(serviceMap); + } catch (e) { + this.logger.error( + 'The gateway did not update its schema due to failed service health checks. ' + + 'The gateway will continue to operate with the previous schema and reattempt updates.' + e + ); + throw e; + } + } + + this.compositionMetadata = result.compositionMetadata; + this.serviceDefinitions = result.serviceDefinitions; + + if (this.queryPlanStore) this.queryPlanStore.flush(); + + const { schema, composedSdl } = this.createSchema(result.serviceDefinitions); + + if (!composedSdl) { + this.logger.error( + "A valid schema couldn't be composed. Falling back to previous schema." + ) + } else { + this.schema = schema; + this.queryPlannerPointer = getQueryPlanner(composedSdl); + + // Notify the schema listeners of the updated schema + try { + this.onSchemaChangeListeners.forEach(listener => listener(this.schema!)); + } catch (e) { + this.logger.error( + "An error was thrown from an 'onSchemaChange' listener. " + + "The schema will still update: " + (e && e.message || e)); + } + + if (this.experimental_didUpdateComposition) { + this.experimental_didUpdateComposition( + { + serviceDefinitions: result.serviceDefinitions, + schema: this.schema, + ...(this.compositionMetadata && { + compositionMetadata: this.compositionMetadata, + }), + }, + previousServiceDefinitions && + previousSchema && { + serviceDefinitions: previousServiceDefinitions, + schema: previousSchema, + ...(previousCompositionMetadata && { + compositionMetadata: previousCompositionMetadata, + }), + }, + ); + } + } + } + + /** + * This can be used without an argument in order to perform an ad-hoc health check + * of the downstream services like so: + * + * @example + * ``` + * try { + * await gateway.serviceHealthCheck(); + * } catch(e) { + * /* your error handling here *\/ + * } + * ``` + * @throws + * @param serviceMap {DataSourceMap} + */ + public serviceHealthCheck(serviceMap: DataSourceMap = this.serviceMap) { + return Promise.all( + Object.entries(serviceMap).map(([name, { dataSource }]) => + dataSource + .process({ request: { query: HEALTH_CHECK_QUERY }, context: {} }) + .then(response => ({ name, response })), + ), + ); + } + + protected createSchema(serviceList: ServiceDefinition[]) { + this.logger.debug( + `Composing schema from service list: \n${serviceList + .map(({ name, url }) => ` ${url || 'local'}: ${name}`) + .join('\n')}`, + ); + + const { schema, errors, composedSdl } = composeAndValidate(serviceList); + + if (errors && errors.length > 0) { + if (this.experimental_didFailComposition) { + this.experimental_didFailComposition({ + errors, + serviceList, + ...(this.compositionMetadata && { + compositionMetadata: this.compositionMetadata, + }), + }); + } + throw new GraphQLSchemaValidationError(errors); + } + + this.createServices(serviceList); + + this.logger.debug('Schema loaded and ready for execution'); + + // This is a workaround for automatic wrapping of all fields, which Apollo + // Server does in the case of implementing resolver wrapping for plugins. + // Here we wrap all fields with support for resolving aliases as part of the + // root value which happens because aliases are resolved by sub services and + // the shape of the root value already contains the aliased fields as + // responseNames + return { schema: wrapSchemaWithAliasResolver(schema), composedSdl }; + } + + public onSchemaChange(callback: SchemaChangeCallback): Unsubscriber { + this.onSchemaChangeListeners.add(callback); + + return () => { + this.onSchemaChangeListeners.delete(callback); + }; + } + + private async pollServices() { + if (this.pollingTimer) clearTimeout(this.pollingTimer); + + // Sleep for the specified pollInterval before kicking off another round of polling + await new Promise(res => { + this.pollingTimer = setTimeout( + () => res(), + this.experimental_pollInterval || 10000, + ); + // Prevent the Node.js event loop from remaining active (and preventing, + // e.g. process shutdown) by calling `unref` on the `Timeout`. For more + // information, see https://nodejs.org/api/timers.html#timers_timeout_unref. + this.pollingTimer?.unref(); + }); + + try { + await this.updateComposition(); + } catch (err) { + this.logger.error(err && err.message || err); + } + + this.pollServices(); + } + + private createAndCacheDataSource( + serviceDef: ServiceEndpointDefinition, + ): GraphQLDataSource { + // If the DataSource has already been created, early return + if ( + this.serviceMap[serviceDef.name] && + serviceDef.url === this.serviceMap[serviceDef.name].url + ) + return this.serviceMap[serviceDef.name].dataSource; + + const dataSource = this.createDataSource(serviceDef); + + // Cache the created DataSource + this.serviceMap[serviceDef.name] = { url: serviceDef.url, dataSource }; + + return dataSource; + } + + private createDataSource( + serviceDef: ServiceEndpointDefinition, + ): GraphQLDataSource { + if (!serviceDef.url && !isLocalConfig(this.config)) { + this.logger.error( + `Service definition for service ${serviceDef.name} is missing a url`, + ); + } + + return this.config.buildService + ? this.config.buildService(serviceDef) + : new RemoteGraphQLDataSource({ + url: serviceDef.url, + }); + } + + protected createServices(services: ServiceEndpointDefinition[]) { + for (const serviceDef of services) { + this.createAndCacheDataSource(serviceDef); + } + } + + protected async loadServiceDefinitions( + config: GatewayConfig, + ): ReturnType { + // This helper avoids the repetition of options in the two cases this method + // is invoked below. It is a helper, rather than an options object, since it + // depends on the presence of `this.engineConfig`, which is guarded against + // further down in this method in two separate places. + const getManagedConfig = (engineConfig: GraphQLServiceEngineConfig) => { + return getServiceDefinitionsFromStorage({ + graphId: engineConfig.graphId, + apiKeyHash: engineConfig.apiKeyHash, + graphVariant: engineConfig.graphVariant, + federationVersion: + (config as ManagedGatewayConfig).federationVersion || 1, + fetcher: this.fetcher, + }); + }; + + if (isLocalConfig(config) || isRemoteConfig(config)) { + if (this.engineConfig && !this.warnedStates.remoteWithLocalConfig) { + // Only display this warning once per start-up. + this.warnedStates.remoteWithLocalConfig = true; + // This error helps avoid common misconfiguration. + // We don't await this because a local configuration should assume + // remote is unavailable for one reason or another. + getManagedConfig(this.engineConfig).then(() => { + this.logger.warn( + "A local gateway service list is overriding an Apollo Graph " + + "Manager managed configuration. To use the managed " + + "configuration, do not specify a service list locally.", + ); + }).catch(() => {}); // Don't mind errors if managed config is missing. + } + } + + if (isLocalConfig(config)) { + return { isNewSchema: false }; + } + + if (isRemoteConfig(config)) { + const serviceList = config.serviceList.map(serviceDefinition => ({ + ...serviceDefinition, + dataSource: this.createAndCacheDataSource(serviceDefinition), + })); + + return getServiceDefinitionsFromRemoteEndpoint({ + serviceList, + ...(config.introspectionHeaders + ? { headers: config.introspectionHeaders } + : {}), + serviceSdlCache: this.serviceSdlCache, + }); + } + + if (!this.engineConfig) { + throw new Error( + 'When `serviceList` is not set, an Apollo Engine configuration must be provided. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information.', + ); + } + + return getManagedConfig(this.engineConfig); + } + + // XXX Nothing guarantees that the only errors thrown or returned in + // result.errors are GraphQLErrors, even though other code (eg + // apollo-engine-reporting) assumes that. In fact, errors talking to backends + // are unlikely to show up as GraphQLErrors. Do we need to use + // formatApolloErrors or something? + public executor = async ( + requestContext: GraphQLRequestContextExecutionDidStart, + ): Promise => { + const { request, document, queryHash, source } = requestContext; + const queryPlanStoreKey = queryHash + (request.operationName || ''); + const operationContext = buildOperationContext({ + schema: this.schema!, + operationDocument: document, + operationString: source, + queryPlannerPointer: this.queryPlannerPointer!, + operationName: request.operationName, + }); + + // No need to build a query plan if we know the request is invalid beforehand + // In the future, this should be controlled by the requestPipeline + const validationErrors = this.validateIncomingRequest( + requestContext, + operationContext, + ); + + if (validationErrors.length > 0) { + return { errors: validationErrors }; + } + + let queryPlan: QueryPlan | undefined; + if (this.queryPlanStore) { + queryPlan = await this.queryPlanStore.get(queryPlanStoreKey); + } + + if (!queryPlan) { + queryPlan = buildQueryPlan(operationContext, { + autoFragmentization: Boolean( + this.config.experimental_autoFragmentization, + ), + }); + if (this.queryPlanStore) { + // The underlying cache store behind the `documentStore` returns a + // `Promise` which is resolved (or rejected), eventually, based on the + // success or failure (respectively) of the cache save attempt. While + // it's certainly possible to `await` this `Promise`, we don't care about + // whether or not it's successful at this point. We'll instead proceed + // to serve the rest of the request and just hope that this works out. + // If it doesn't work, the next request will have another opportunity to + // try again. Errors will surface as warnings, as appropriate. + // + // While it shouldn't normally be necessary to wrap this `Promise` in a + // `Promise.resolve` invocation, it seems that the underlying cache store + // is returning a non-native `Promise` (e.g. Bluebird, etc.). + Promise.resolve( + this.queryPlanStore.set(queryPlanStoreKey, queryPlan), + ).catch(err => + this.logger.warn( + 'Could not store queryPlan' + ((err && err.message) || err), + ), + ); + } + } + + const serviceMap: ServiceMap = Object.entries(this.serviceMap).reduce( + (serviceDataSources, [serviceName, { dataSource }]) => { + serviceDataSources[serviceName] = dataSource; + return serviceDataSources; + }, + Object.create(null) as ServiceMap, + ); + + if (this.experimental_didResolveQueryPlan) { + this.experimental_didResolveQueryPlan({ + queryPlan, + serviceMap, + requestContext, + operationContext, + }); + } + + const response = await executeQueryPlan( + queryPlan, + serviceMap, + requestContext, + operationContext, + ); + + const shouldShowQueryPlan = + this.config.__exposeQueryPlanExperimental && + request.http && + request.http.headers && + request.http.headers.get('Apollo-Query-Plan-Experimental'); + + // We only want to serialize the query plan if we're going to use it, which is + // in two cases: + // 1) non-empty query plan and config.debug === true + // 2) non-empty query plan and shouldShowQueryPlan === true + const serializedQueryPlan = + queryPlan.node && (this.config.debug || shouldShowQueryPlan) + ? serializeQueryPlan(queryPlan) + : null; + + if (this.config.debug && serializedQueryPlan) { + this.logger.debug(serializedQueryPlan); + } + + if (shouldShowQueryPlan) { + // TODO: expose the query plan in a more flexible JSON format in the future + // and rename this to `queryPlan`. Playground should cutover to use the new + // option once we've built a way to print that representation. + + // In the case that `serializedQueryPlan` is null (on introspection), we + // still want to respond to Playground with something truthy since it depends + // on this to decide that query plans are supported by this gateway. + response.extensions = { + __queryPlanExperimental: serializedQueryPlan || true, + }; + } + return response; + }; + + protected validateIncomingRequest( + requestContext: GraphQLRequestContextExecutionDidStart, + operationContext: OperationContext, + ) { + // casting out of `readonly` + const variableDefinitions = operationContext.operation + .variableDefinitions as VariableDefinitionNode[] | undefined; + + if (!variableDefinitions) return []; + + const { errors } = getVariableValues( + operationContext.schema, + variableDefinitions, + requestContext.request.variables!, + ); + + return errors || []; + } + + private initializeQueryPlanStore(): void { + this.queryPlanStore = new InMemoryLRUCache({ + // Create ~about~ a 30MiB InMemoryLRUCache. This is less than precise + // since the technique to calculate the size of a DocumentNode is + // only using JSON.stringify on the DocumentNode (and thus doesn't account + // for unicode characters, etc.), but it should do a reasonable job at + // providing a caching document store for most operations. + maxSize: + Math.pow(2, 20) * + (this.experimental_approximateQueryPlanStoreMiB || 30), + sizeCalculator: approximateObjectSize, + }); + } + + public async stop() { + if (this.pollingTimer) { + clearTimeout(this.pollingTimer); + this.pollingTimer = undefined; + } + } +} + +function approximateObjectSize(obj: T): number { + return Buffer.byteLength(JSON.stringify(obj), 'utf8'); +} + +// We can't use transformSchema here because the extension data for query +// planning would be lost. Instead we set a resolver for each field +// in order to counteract GraphQLExtensions preventing a defaultFieldResolver +// from doing the same job +function wrapSchemaWithAliasResolver( + schema: ComposedGraphQLSchema, +): ComposedGraphQLSchema { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type = typeMap[typeName]; + + if (isObjectType(type) && !isIntrospectionType(type)) { + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + field.resolve = defaultFieldResolverWithAliasSupport; + }); + } + }); + return schema; +} + +export { + buildQueryPlan, + executeQueryPlan, + serializeQueryPlan, + buildOperationContext, + QueryPlan, + ServiceMap, +}; +export * from './datasources'; diff --git a/gateway-js/src/loadServicesFromRemoteEndpoint.ts b/gateway-js/src/loadServicesFromRemoteEndpoint.ts new file mode 100644 index 000000000..c4dd8b9f3 --- /dev/null +++ b/gateway-js/src/loadServicesFromRemoteEndpoint.ts @@ -0,0 +1,76 @@ +import { GraphQLRequest } from 'apollo-server-types'; +import { parse } from 'graphql'; +import { Headers, HeadersInit } from 'node-fetch'; +import { GraphQLDataSource } from './datasources/types'; +import { Experimental_UpdateServiceDefinitions, SERVICE_DEFINITION_QUERY } from './'; +import { ServiceDefinition } from '@apollo/federation'; + +export async function getServiceDefinitionsFromRemoteEndpoint({ + serviceList, + headers = {}, + serviceSdlCache, +}: { + serviceList: { + name: string; + url?: string; + dataSource: GraphQLDataSource; + }[]; + headers?: HeadersInit; + serviceSdlCache: Map; +}): ReturnType { + if (!serviceList || !serviceList.length) { + throw new Error( + 'Tried to load services from remote endpoints but none provided', + ); + } + + let isNewSchema = false; + // for each service, fetch its introspection schema + const promiseOfServiceList = serviceList.map(({ name, url, dataSource }) => { + if (!url) { + throw new Error( + `Tried to load schema for '${name}' but no 'url' was specified.`); + } + + const request: GraphQLRequest = { + query: SERVICE_DEFINITION_QUERY, + http: { + url, + method: 'POST', + headers: new Headers(headers), + }, + }; + + return dataSource + .process({ request, context: {} }) + .then(({ data, errors }): ServiceDefinition => { + if (data && !errors) { + const typeDefs = data._service.sdl as string; + const previousDefinition = serviceSdlCache.get(name); + // this lets us know if any downstream service has changed + // and we need to recalculate the schema + if (previousDefinition !== typeDefs) { + isNewSchema = true; + } + serviceSdlCache.set(name, typeDefs); + return { + name, + url, + typeDefs: parse(typeDefs), + }; + } + + throw new Error(errors?.map(e => e.message).join("\n")); + }) + .catch(err => { + const errorMessage = + `Couldn't load service definitions for "${name}" at ${url}` + + (err && err.message ? ": " + err.message || err : ""); + + throw new Error(errorMessage); + }); + }); + + const serviceDefinitions = await Promise.all(promiseOfServiceList); + return { serviceDefinitions, isNewSchema } +} diff --git a/gateway-js/src/loadServicesFromStorage.ts b/gateway-js/src/loadServicesFromStorage.ts new file mode 100644 index 000000000..08ada2fdf --- /dev/null +++ b/gateway-js/src/loadServicesFromStorage.ts @@ -0,0 +1,170 @@ +import { fetch } from 'apollo-server-env'; +import { parse } from 'graphql'; +import { Experimental_UpdateServiceDefinitions } from '.'; + +interface LinkFileResult { + configPath: string; + formatVersion: number; +} + +interface ImplementingService { + formatVersion: number; + graphID: string; + graphVariant: string; + name: string; + revision: string; + url: string; + partialSchemaPath: string; +} + +interface ImplementingServiceLocation { + name: string; + path: string; +} + +export interface CompositionMetadata { + formatVersion: number; + id: string; + implementingServiceLocations: ImplementingServiceLocation[]; + schemaHash: string; +} + +const envOverridePartialSchemaBaseUrl = 'APOLLO_PARTIAL_SCHEMA_BASE_URL'; +const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; + +const urlFromEnvOrDefault = (envKey: string, fallback: string) => + (process.env[envKey] || fallback).replace(/\/$/, ''); + +// Generate and cache our desired operation manifest URL. +const urlPartialSchemaBase = urlFromEnvOrDefault( + envOverridePartialSchemaBaseUrl, + 'https://federation.api.apollographql.com/', +); + +const urlStorageSecretBase: string = urlFromEnvOrDefault( + envOverrideStorageSecretBaseUrl, + 'https://storage-secrets.api.apollographql.com/', +); + +function getStorageSecretUrl(graphId: string, apiKeyHash: string): string { + return `${urlStorageSecretBase}/${graphId}/storage-secret/${apiKeyHash}.json`; +} + +function fetchApolloGcs( + fetcher: typeof fetch, + ...args: Parameters +): ReturnType { + const [input, init] = args; + + // Used in logging. + const url = typeof input === 'object' && input.url || input; + + return fetcher(input, init) + .catch(fetchError => { + throw new Error( + "Cannot access Apollo Graph Manager storage: " + fetchError) + }) + .then(async (response) => { + // If the fetcher has a cache and has implemented ETag validation, then + // a 304 response may be returned. Either way, we will return the + // non-JSON-parsed version and let the caller decide if that's important + // to their needs. + if (response.ok || response.status === 304) { + return response; + } + + // We won't make any assumptions that the body is anything but text, to + // avoid parsing errors in this unknown condition. + const body = await response.text(); + + // Google Cloud Storage returns an `application/xml` error under error + // conditions. We'll special-case our known errors, and resort to + // printing the body for others. + if ( + response.headers.get('content-type') === 'application/xml' && + response.status === 403 && + body.includes("AccessDenied") && + body.includes("Anonymous caller does not have storage.objects.get") + ) { + throw new Error( + "Unable to authenticate with Apollo Graph Manager storage " + + "while fetching " + url + ". Ensure that the API key is " + + "configured properly and that a federated service has been " + + "pushed. For details, see " + + "https://go.apollo.dev/g/resolve-access-denied."); + } + + // Normally, we'll try to keep the logs clean with errors we expect. + // If it's not a known error, reveal the full body for debugging. + throw new Error( + "Could not communicate with Apollo Graph Manager storage: " + body); + }); +}; + +export async function getServiceDefinitionsFromStorage({ + graphId, + apiKeyHash, + graphVariant, + federationVersion, + fetcher, +}: { + graphId: string; + apiKeyHash: string; + graphVariant?: string; + federationVersion: number; + fetcher: typeof fetch; +}): ReturnType { + // fetch the storage secret + const storageSecretUrl = getStorageSecretUrl(graphId, apiKeyHash); + + // The storage secret is a JSON string (e.g. `"secret"`). + const secret: string = + await fetchApolloGcs(fetcher, storageSecretUrl).then(res => res.json()); + + if (!graphVariant) { + graphVariant = 'current'; + } + + const baseUrl = `${urlPartialSchemaBase}/${secret}/${graphVariant}/v${federationVersion}`; + + const compositionConfigResponse = + await fetchApolloGcs(fetcher, `${baseUrl}/composition-config-link`); + + if (compositionConfigResponse.status === 304) { + return { isNewSchema: false }; + } + + const linkFileResult: LinkFileResult = await compositionConfigResponse.json(); + + const compositionMetadata: CompositionMetadata = await fetchApolloGcs( + fetcher, + `${urlPartialSchemaBase}/${linkFileResult.configPath}`, + ).then(res => res.json()); + + // It's important to maintain the original order here + const serviceDefinitions = await Promise.all( + compositionMetadata.implementingServiceLocations.map( + async ({ name, path }) => { + const { url, partialSchemaPath }: ImplementingService = await fetcher( + `${urlPartialSchemaBase}/${path}`, + ).then(response => response.json()); + + const sdl = await fetcher( + `${urlPartialSchemaBase}/${partialSchemaPath}`, + ).then(response => response.text()); + + return { name, url, typeDefs: parse(sdl) }; + }, + ), + ); + + // explicity return that this is a new schema, as the link file has changed. + // we can't use the hit property of the fetchPartialSchemaFiles, as the partial + // schema may all be cache hits with the final schema still being new + // (for instance if a partial schema is removed or a partial schema is rolled back to a prior version, which is still in cache) + return { + serviceDefinitions, + compositionMetadata, + isNewSchema: true, + }; +} diff --git a/gateway-js/src/make-fetch-happen.d.ts b/gateway-js/src/make-fetch-happen.d.ts new file mode 100644 index 000000000..6b7f1737a --- /dev/null +++ b/gateway-js/src/make-fetch-happen.d.ts @@ -0,0 +1,59 @@ +/** + * We are attempting to get types included natively in this package, but it + * has not happened, yet! + * + * See https://github.com/npm/make-fetch-happen/issues/20 + */ +declare module 'make-fetch-happen' { + import { + Response, + Request, + RequestInfo, + RequestInit, + } from 'apollo-server-env'; + + // If adding to these options, they should mirror those from `make-fetch-happen` + // @see: https://github.com/npm/make-fetch-happen/#extra-options + export interface FetcherOptions { + cacheManager?: string | CacheManager; + // @see: https://www.npmjs.com/package/retry#retrytimeoutsoptions + retry?: + | boolean + | number + | { + // The maximum amount of times to retry the operation. Default is 10. Seting this to 1 means do it once, then retry it once + retries?: number; + // The exponential factor to use. Default is 2. + factor?: number; + // The number of milliseconds before starting the first retry. Default is 1000. + minTimeout?: number; + // The maximum number of milliseconds between two retries. Default is Infinity. + maxTimeout?: number; + // Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false. + randomize?: boolean; + }; + onRetry?(): void; + } + + export interface CacheManager { + delete(req: Request): Promise; + put(req: Request, res: Response): Promise; + match(req: Request): Promise; + } + + /** + * This is an augmentation of the fetch function types provided by `apollo-server-env` + * @see: https://git.io/JvBwX + */ + export interface Fetcher { + (input?: RequestInfo, init?: RequestInit & FetcherOptions): Promise< + Response + >; + } + + let fetch: Fetcher & { + defaults(opts?: RequestInit & FetcherOptions): Fetcher; + }; + + export default fetch; +} diff --git a/gateway-js/src/snapshotSerializers/astSerializer.ts b/gateway-js/src/snapshotSerializers/astSerializer.ts new file mode 100644 index 000000000..f3cee95e7 --- /dev/null +++ b/gateway-js/src/snapshotSerializers/astSerializer.ts @@ -0,0 +1,116 @@ +import { ASTNode, print, Kind, visit } from 'graphql'; +import { Plugin, Config, Refs } from 'pretty-format'; +import { QueryPlanSelectionNode, QueryPlanInlineFragmentNode } from '../QueryPlan'; +import { SelectionNode as GraphQLJSSelectionNode } from 'graphql'; + +export default { + test(value: any) { + return value && typeof value.kind === 'string'; + }, + + serialize( + value: ASTNode, + _config: Config, + indentation: string, + _depth: number, + _refs: Refs, + _printer: any, + ): string { + return print(remapInlineFragmentNodes(value)) + .trim() + .replace(/\n\n/g, '\n') + .replace(/\n/g, '\n' + indentation); + }, +} as Plugin; + +/** + * This function converts potential InlineFragmentNodes that WE created + * (defined in ../QueryPlan, not graphql-js) to GraphQL-js compliant AST nodes + * for the graphql-js printer to work with + * + * The arg type here SHOULD be (node: AstNode | SelectionNode (from ../QueryPlan)), + * but that breaks the graphql-js visitor, as it won't allow our redefined + * SelectionNode to be passed in. + * + * Since our SelectionNode still has a `kind`, this will still functionally work + * at runtime to call the InlineFragment visitor defined below + * + * We have to cast the `fragmentNode as unknown` and then to an InlineFragmentNode + * at the bottom though, since there's no way to cast it appropriately to an + * `InlineFragmentNode` as defined in ../QueryPlan.ts. TypeScript will complain + * about there not being overlapping fields + */ +export function remapInlineFragmentNodes(node: ASTNode): ASTNode { + return visit(node, { + InlineFragment: (fragmentNode) => { + // if the fragmentNode is already a proper graphql AST Node, return it + if (fragmentNode.selectionSet) return fragmentNode; + + /** + * Since the above check wasn't hit, we _know_ that fragmentNode is an + * InlineFragmentNode from ../QueryPlan, but we can't actually type that + * without causing ourselves a lot of headache, so we cast to unknown and + * then to InlineFragmentNode (from ../QueryPlan) below + */ + + // if the fragmentNode is a QueryPlan InlineFragmentNode, convert it to graphql-js node + return { + kind: Kind.INLINE_FRAGMENT, + typeCondition: fragmentNode.typeCondition + ? { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: fragmentNode.typeCondition, + }, + } + : undefined, + selectionSet: { + kind: Kind.SELECTION_SET, + // we have to recursively rebuild the selectionSet using selections + selections: remapSelections( + ((fragmentNode as unknown) as QueryPlanInlineFragmentNode).selections, + ), + }, + }; + }, + }); +} + +function remapSelections( + selections: QueryPlanSelectionNode[], +): ReadonlyArray { + return selections.map((selection) => { + switch (selection.kind) { + case Kind.FIELD: + return { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: selection.name, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: remapSelections(selection.selections || []), + }, + }; + case Kind.INLINE_FRAGMENT: + return { + kind: Kind.INLINE_FRAGMENT, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: remapSelections(selection.selections || []), + }, + typeCondition: selection.typeCondition + ? { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: selection.typeCondition, + }, + } + : undefined, + }; + } + }); +} diff --git a/gateway-js/src/snapshotSerializers/index.ts b/gateway-js/src/snapshotSerializers/index.ts new file mode 100644 index 000000000..3a768cfc1 --- /dev/null +++ b/gateway-js/src/snapshotSerializers/index.ts @@ -0,0 +1,21 @@ +import astSerializer from './astSerializer'; +import selectionSetSerializer from './selectionSetSerializer'; +import typeSerializer from './typeSerializer'; +import queryPlanSerializer from './queryPlanSerializer'; +export { + astSerializer, + selectionSetSerializer, + typeSerializer, + queryPlanSerializer, +}; + +declare global { + namespace jest { + interface Expect { + /** + * Adds a module to format application-specific data structures for serialization. + */ + addSnapshotSerializer(serializer: import('pretty-format').Plugin): void; + } + } +} diff --git a/gateway-js/src/snapshotSerializers/queryPlanSerializer.ts b/gateway-js/src/snapshotSerializers/queryPlanSerializer.ts new file mode 100644 index 000000000..08023fe2e --- /dev/null +++ b/gateway-js/src/snapshotSerializers/queryPlanSerializer.ts @@ -0,0 +1,152 @@ +import { Config, Plugin, Refs } from 'pretty-format'; +import { PlanNode, QueryPlan } from '../QueryPlan'; +import { parse, Kind, visit, DocumentNode } from 'graphql'; + +export default { + test(value: any) { + return value && value.kind === 'QueryPlan'; + }, + + serialize( + queryPlan: QueryPlan, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: any, + ): string { + return ( + 'QueryPlan {' + + printNodes( + queryPlan.node ? [queryPlan.node] : undefined, + config, + indentation, + depth, + refs, + printer, + ) + + '}' + ); + }, +} as Plugin; + +function printNode( + node: PlanNode, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: any, +): string { + let result = ''; + + const indentationNext = indentation + config.indent; + + switch (node.kind) { + case 'Fetch': + result += + `Fetch(service: "${node.serviceName}")` + + ' {' + + config.spacingOuter + + indentationNext + + (node.requires + ? printer( + // this is an array of selections, so we need to make it a proper + // selectionSet so we can print it + { kind: Kind.SELECTION_SET, selections: node.requires }, + config, + indentationNext, + depth, + refs, + printer, + ) + + ' =>' + + config.spacingOuter + + indentationNext + : '') + + printer( + flattenEntitiesField(parse(node.operation)), + config, + indentationNext, + depth, + refs, + printer, + ) + + config.spacingOuter + + indentation + + '}'; + break; + case 'Flatten': + result += `Flatten(path: "${node.path.join('.')}")`; + break; + default: + result += node.kind; + } + + const nodes = + 'nodes' in node ? node.nodes : 'node' in node ? [node.node] : []; + + if (nodes.length > 0) { + result += + ' {' + printNodes(nodes, config, indentation, depth, refs, printer) + '}'; + } + + return result; +} + +function printNodes( + nodes: PlanNode[] | undefined, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: any, +): string { + let result = ''; + + if (nodes && nodes.length > 0) { + result += config.spacingOuter; + + const indentationNext = indentation + config.indent; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (!node) continue; + + result += + indentationNext + + printNode(node, config, indentationNext, depth, refs, printer); + + if (i < nodes.length - 1) { + result += ',' + config.spacingInner; + } else if (!config.min) { + result += ','; + } + } + + result += config.spacingOuter + indentation; + } + + return result; +} + +/** + * when we serialize a query plan, we want to serialize the operation, but not + * show the root level `query` definition or the `_entities` call. This function + * flattens those nodes to only show their selectionSets + */ +function flattenEntitiesField(node: DocumentNode) { + return visit(node, { + OperationDefinition: ({ operation, selectionSet }) => { + const firstSelection = selectionSet.selections[0]; + if ( + operation === 'query' && + firstSelection.kind === Kind.FIELD && + firstSelection.name.value === '_entities' + ) { + return firstSelection.selectionSet; + } + // we don't want to print the `query { }` definition either for query plan printing + return selectionSet; + }, + }); +} diff --git a/gateway-js/src/snapshotSerializers/selectionSetSerializer.ts b/gateway-js/src/snapshotSerializers/selectionSetSerializer.ts new file mode 100644 index 000000000..33dfba417 --- /dev/null +++ b/gateway-js/src/snapshotSerializers/selectionSetSerializer.ts @@ -0,0 +1,13 @@ +import { print, SelectionNode, isSelectionNode } from 'graphql'; +import { Plugin } from 'pretty-format'; + +export default { + test(value: any) { + return ( + Array.isArray(value) && value.length > 0 && value.every(isSelectionNode) + ); + }, + print(selectionNodes: SelectionNode[]): string { + return selectionNodes.map(node => print(node)).join('\n'); + }, +} as Plugin; diff --git a/gateway-js/src/snapshotSerializers/typeSerializer.ts b/gateway-js/src/snapshotSerializers/typeSerializer.ts new file mode 100644 index 000000000..7b78e18ca --- /dev/null +++ b/gateway-js/src/snapshotSerializers/typeSerializer.ts @@ -0,0 +1,11 @@ +import { isNamedType, GraphQLNamedType, printType } from 'graphql'; +import { Plugin } from 'pretty-format'; + +export default { + test(value: any) { + return value && isNamedType(value); + }, + print(value: GraphQLNamedType) { + return printType(value); + }, +} as Plugin; diff --git a/gateway-js/src/utilities/__tests__/deepMerge.test.ts b/gateway-js/src/utilities/__tests__/deepMerge.test.ts new file mode 100644 index 000000000..545968ba9 --- /dev/null +++ b/gateway-js/src/utilities/__tests__/deepMerge.test.ts @@ -0,0 +1,77 @@ +import { deepMerge } from '../deepMerge'; + +describe('deepMerge', () => { + it('merges basic', () => { + const target = { + a: 1, + b: 2, + }; + + const source = { + b: 3, + c: 4, + }; + + expect(deepMerge(target, source)).toEqual({ + a: 1, + b: 3, + c: 4, + }); + }); + + it('merges nested objects', () => { + const target = { + a: 1, + b: { + someProperty: 1, + overwrittenProperty: 'clean', + }, + }; + + const source = { + b: { + overwrittenProperty: 'dirty', + newProperty: 'new', + }, + c: 4, + }; + + expect(deepMerge(target, source)).toEqual({ + a: 1, + b: { + newProperty: 'new', + overwrittenProperty: 'dirty', + someProperty: 1, + }, + c: 4, + }); + }); + + it('ignores merging __proto__ fields', () => { + const target = {}; + + // Bypass setters on __proto__ + const source = JSON.parse('{"__proto__": {"pollution": true}}'); + deepMerge(target, source); + + expect(Object.prototype.hasOwnProperty('pollution')).toBe(false); + }); + + it('merges arrays', () => { + const target = { + a: 1, + b: [{ c: 1, d: 2 }], + }; + + const source = { + e: 2, + b: [{ f: 3 }], + }; + + expect(deepMerge(target, source)).toEqual({ + a: 1, + e: 2, + b: [{ c: 1, d: 2, f: 3 }], + }); + }); +}); diff --git a/gateway-js/src/utilities/array.ts b/gateway-js/src/utilities/array.ts new file mode 100644 index 000000000..1141d4207 --- /dev/null +++ b/gateway-js/src/utilities/array.ts @@ -0,0 +1,55 @@ +function isNotNullOrUndefined( + value: T | null | undefined, +): value is T { + return value !== null && typeof value !== 'undefined'; +} + +export function compactMap( + array: T[], + callbackfn: (value: T, index: number, array: T[]) => U | null | undefined, +): U[] { + return array.reduce( + (accumulator, element, index, array) => { + const result = callbackfn(element, index, array); + if (isNotNullOrUndefined(result)) { + accumulator.push(result); + } + return accumulator; + }, + [] as U[], + ); +} + +export function findAndExtract( + array: T[], + predicate: (element: T, index: number, array: T[]) => boolean, +): [T | undefined, T[]] { + const index = array.findIndex(predicate); + if (index === -1) return [undefined, array]; + + let remaining = array.slice(0, index); + if (index < array.length - 1) { + remaining.push(...array.slice(index + 1)); + } + + return [array[index], remaining]; +} + +export function groupBy(keyFunction: (element: T) => U) { + return (iterable: Iterable) => { + const result = new Map(); + + for (const element of iterable) { + const key = keyFunction(element); + const group = result.get(key); + + if (group) { + group.push(element); + } else { + result.set(key, [element]); + } + } + + return result; + }; +} diff --git a/gateway-js/src/utilities/deepMerge.ts b/gateway-js/src/utilities/deepMerge.ts new file mode 100644 index 000000000..fb5504a38 --- /dev/null +++ b/gateway-js/src/utilities/deepMerge.ts @@ -0,0 +1,30 @@ +import { isObject } from './predicates'; + +export function deepMerge(target: any, source: any): any { + if (source === undefined || source === null) return target; + + for (const key of Object.keys(source)) { + if (source[key] === undefined || key === '__proto__') continue; + + if (target[key] && isObject(source[key])) { + deepMerge(target[key], source[key]); + } else if ( + Array.isArray(source[key]) && + Array.isArray(target[key]) && + source[key].length === target[key].length + ) { + let i = 0; + for (; i < source[key].length; i++) { + if (isObject(target[key][i]) && isObject(source[key][i])) { + deepMerge(target[key][i], source[key][i]); + } else { + target[key][i] = source[key][i]; + } + } + } else { + target[key] = source[key]; + } + } + + return target; +} diff --git a/gateway-js/src/utilities/graphql.ts b/gateway-js/src/utilities/graphql.ts new file mode 100644 index 000000000..44feedf81 --- /dev/null +++ b/gateway-js/src/utilities/graphql.ts @@ -0,0 +1,54 @@ +import { + ASTNode, + FieldNode, + GraphQLNullableType, + GraphQLType, + isListType, + isNonNullType, + Kind, + ListTypeNode, + NamedTypeNode, + OperationDefinitionNode, + parse, + print, + SelectionNode, + TypeNode, +} from 'graphql'; + +export function getResponseName(node: FieldNode): string { + return node.alias ? node.alias.value : node.name.value; +} + +export function allNodesAreOfSameKind( + firstNode: T, + remainingNodes: ASTNode[], +): remainingNodes is T[] { + return !remainingNodes.some(node => node.kind !== firstNode.kind); +} + +export function astFromType( + type: GraphQLNullableType, +): NamedTypeNode | ListTypeNode; +export function astFromType(type: GraphQLType): TypeNode { + if (isListType(type)) { + return { kind: Kind.LIST_TYPE, type: astFromType(type.ofType) }; + } else if (isNonNullType(type)) { + return { kind: Kind.NON_NULL_TYPE, type: astFromType(type.ofType) }; + } else { + return { + kind: Kind.NAMED_TYPE, + name: { kind: Kind.NAME, value: type.name }, + }; + } +} + +export function printWithReducedWhitespace(ast: ASTNode): string { + return print(ast) + .replace(/\s+/g, ' ') + .trim(); +} + +export function parseSelections(source: string): ReadonlyArray { + return (parse(`query { ${source} }`) + .definitions[0] as OperationDefinitionNode).selectionSet.selections; +} diff --git a/gateway-js/src/utilities/predicates.ts b/gateway-js/src/utilities/predicates.ts new file mode 100644 index 000000000..d9a73b8e7 --- /dev/null +++ b/gateway-js/src/utilities/predicates.ts @@ -0,0 +1,8 @@ +export function isObject(value: any): value is object { + return ( + value !== undefined && + value !== null && + typeof value === 'object' && + !Array.isArray(value) + ); +} diff --git a/gateway-js/tsconfig.json b/gateway-js/tsconfig.json new file mode 100644 index 000000000..ace2d72ea --- /dev/null +++ b/gateway-js/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "target": "es2019", + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__", "**/__mocks__"], + "references": [ + { "path": "../federation-js" }, + { "path": "../federation-integration-testsuite-js" }, + ] +} diff --git a/jest.config.base.js b/jest.config.base.js new file mode 100644 index 000000000..d8ba10c58 --- /dev/null +++ b/jest.config.base.js @@ -0,0 +1,27 @@ +const { defaults } = require("jest-config"); + +module.exports = { + testEnvironment: "node", + preset: "ts-jest", + testMatch: null, + testRegex: "/__tests__/.*\\.test\\.(js|ts)$", + testPathIgnorePatterns: [ + "/node_modules/", + "/dist/" + ], + moduleFileExtensions: [...defaults.moduleFileExtensions, "ts", "tsx"], + moduleNameMapper: { + '^__mocks__/(.*)$': '/src/__mocks__/$1', + // This regex should match the packages that we want compiled from source + // through `ts-jest`, as opposed to loaded from their output files in + // `dist`. + '^((?:federation-js|gateway-js)[^/]*)(?:/dist)?((?:/.*)|$)': '/../$1/src$2' + }, + clearMocks: true, + globals: { + "ts-jest": { + tsConfig: "/src/__tests__/tsconfig.json", + diagnostics: false + } + } +}; diff --git a/lerna.json b/lerna.json new file mode 100644 index 000000000..5439c6c34 --- /dev/null +++ b/lerna.json @@ -0,0 +1,11 @@ +{ + "packages": ["federation-js", "federation-integration-testsuite-js", "gateway-js"], + "version": "independent", + "command": { + "version": { + "message": "Release", + "ignoreChanges": ["**/*.md"], + "includeMergedTags": true + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..69c16ec4e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15955 @@ +{ + "name": "apollo-federation-monorepo", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@apollo/federation": { + "version": "file:federation-js", + "requires": { + "apollo-graphql": "^0.6.0", + "apollo-server-env": "^2.4.5", + "core-js": "^3.4.0", + "lodash.xorby": "^4.7.0" + } + }, + "@apollo/gateway": { + "version": "file:gateway-js", + "requires": { + "@apollo/federation": "file:federation-js", + "@apollo/query-planner-wasm": "0.0.2", + "@types/node-fetch": "2.5.4", + "apollo-engine-reporting-protobuf": "^0.5.2", + "apollo-graphql": "^0.6.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-core": "^2.17.0", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-types": "^0.5.1", + "loglevel": "^1.6.1", + "make-fetch-happen": "^8.0.0", + "pretty-format": "^26.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + }, + "@types/node-fetch": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz", + "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==", + "requires": { + "@types/node": "*" + } + } + } + }, + "@apollo/protobufjs": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.5.tgz", + "integrity": "sha512-ZtyaBH1icCgqwIGb3zrtopV2D5Q8yxibkJzlaViM08eOhTQc7rACdYu0pfORFfhllvdMZ3aq69vifYHszY4gNA==", + "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.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.29.tgz", + "integrity": "sha512-zLo9rjUeQ5+QVhOufDwrb3XKyso31fJBJnk9wUUQIBDExF/O4LryvpOfozfUaxgqifTnlt7FyqsAPXUq5yFZSA==" + } + } + }, + "@apollo/query-planner-wasm": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@apollo/query-planner-wasm/-/query-planner-wasm-0.0.2.tgz", + "integrity": "sha512-qkth7quSzSXEujJ8FXnMPtVh4v/V2fbv/LNHAayBMKdNxb/ZKPaSYgQSx1Y87Alli/GOwS3Jc5j+0KplqgHrVQ==" + }, + "@apollographql/apollo-tools": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.8.tgz", + "integrity": "sha512-W2+HB8Y7ifowcf3YyPHgDI05izyRtOeZ4MqIr7LbTArtmJ0ZHULWpn84SGMW7NAvTV1tFExpHlveHhnXuJfuGA==", + "requires": { + "apollo-env": "^0.6.5" + } + }, + "@apollographql/graphql-playground-html": { + "version": "1.6.26", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.26.tgz", + "integrity": "sha512-XAwXOIab51QyhBxnxySdK3nuMEUohhDsHQ5Rbco/V1vjlP75zZ0ZLHD9dTpXTN8uxKxopb2lUvJTq+M4g2Q0HQ==", + "requires": { + "xss": "^1.0.6" + } + }, + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.6", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.5", + "@babel/types": "^7.11.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "dev": true, + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@evocateur/libnpmaccess": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", + "integrity": "sha512-KSCAHwNWro0CF2ukxufCitT9K5LjL/KuMmNzSu8wuwN2rjyKHD8+cmOsiybK+W5hdnwc5M1SmRlVCaMHQo+3rg==", + "dev": true, + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0" + }, + "dependencies": { + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + } + } + }, + "@evocateur/libnpmpublish": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@evocateur/libnpmpublish/-/libnpmpublish-1.2.2.tgz", + "integrity": "sha512-MJrrk9ct1FeY9zRlyeoyMieBjGDG9ihyyD9/Ft6MMrTxql9NyoEx2hw9casTIP4CdqEVu+3nQ2nXxoJ8RCXyFg==", + "dev": true, + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "semver": "^5.5.1", + "ssri": "^6.0.1" + }, + "dependencies": { + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + } + } + }, + "@evocateur/npm-registry-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@evocateur/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz", + "integrity": "sha512-k1WGfKRQyhJpIr+P17O5vLIo2ko1PFLKwoetatdduUSt/aQ4J2sJrJwwatdI5Z3SiYk/mRH9S3JpdmMFd/IK4g==", + "dev": true, + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "dev": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "make-fetch-happen": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", + "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "dev": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dev": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dev": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + } + } + }, + "@evocateur/pacote": { + "version": "9.6.5", + "resolved": "https://registry.npmjs.org/@evocateur/pacote/-/pacote-9.6.5.tgz", + "integrity": "sha512-EI552lf0aG2nOV8NnZpTxNo2PcXKPmDbF9K8eCBFQdIZwHNGN/mi815fxtmUMa2wTa1yndotICIDt/V0vpEx2w==", + "dev": true, + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "bluebird": "^3.5.3", + "cacache": "^12.0.3", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.5.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.4.4", + "npm-pick-manifest": "^3.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.3", + "safe-buffer": "^5.2.0", + "semver": "^5.7.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "dev": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "make-fetch-happen": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", + "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "dev": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dev": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dev": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.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", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", + "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-message-util": "^25.5.0", + "jest-util": "^25.5.0", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/core": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", + "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/reporters": "^25.5.1", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^25.5.0", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-resolve-dependencies": "^25.5.4", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "jest-watcher": "^25.5.0", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "realpath-native": "^2.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/environment": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", + "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/fake-timers": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", + "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "lolex": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/globals": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", + "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/types": "^25.5.0", + "expect": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/reporters": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", + "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^25.5.1", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "node-notifier": "^6.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^3.1.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^4.1.3" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/source-map": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", + "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", + "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/types": "^25.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/test-sequencer": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", + "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", + "dev": true, + "requires": { + "@jest/test-result": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4" + } + }, + "@jest/transform": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", + "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^25.5.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-regex-util": "^25.2.6", + "jest-util": "^25.5.0", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/types": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@lerna/add": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz", + "integrity": "sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A==", + "dev": true, + "requires": { + "@evocateur/pacote": "^9.6.3", + "@lerna/bootstrap": "3.21.0", + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "npm-package-arg": "^6.1.0", + "p-map": "^2.1.0", + "semver": "^6.2.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/bootstrap": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz", + "integrity": "sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/has-npm-version": "3.16.5", + "@lerna/npm-install": "3.16.5", + "@lerna/package-graph": "3.18.5", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/rimraf-dir": "3.16.5", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.5", + "@lerna/symlink-binary": "3.17.0", + "@lerna/symlink-dependencies": "3.17.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "get-port": "^4.2.0", + "multimatch": "^3.0.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0", + "read-package-tree": "^5.1.6", + "semver": "^6.2.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/changed": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz", + "integrity": "sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw==", + "dev": true, + "requires": { + "@lerna/collect-updates": "3.20.0", + "@lerna/command": "3.21.0", + "@lerna/listable": "3.18.5", + "@lerna/output": "3.13.0" + } + }, + "@lerna/check-working-tree": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz", + "integrity": "sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ==", + "dev": true, + "requires": { + "@lerna/collect-uncommitted": "3.16.5", + "@lerna/describe-ref": "3.16.5", + "@lerna/validation-error": "3.13.0" + } + }, + "@lerna/child-process": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz", + "integrity": "sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg==", + "dev": true, + "requires": { + "chalk": "^2.3.1", + "execa": "^1.0.0", + "strong-log-transformer": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@lerna/clean": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz", + "integrity": "sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/prompt": "3.18.5", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/rimraf-dir": "3.16.5", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/cli": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.5.tgz", + "integrity": "sha512-erkbxkj9jfc89vVs/jBLY/fM0I80oLmJkFUV3Q3wk9J3miYhP14zgVEBsPZY68IZlEjT6T3Xlq2xO1AVaatHsA==", + "dev": true, + "requires": { + "@lerna/global-options": "3.13.0", + "dedent": "^0.7.0", + "npmlog": "^4.1.2", + "yargs": "^14.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "@lerna/collect-uncommitted": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz", + "integrity": "sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "chalk": "^2.3.1", + "figgy-pudding": "^3.5.1", + "npmlog": "^4.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@lerna/collect-updates": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.20.0.tgz", + "integrity": "sha512-qBTVT5g4fupVhBFuY4nI/3FSJtQVcDh7/gEPOpRxoXB/yCSnT38MFHXWl+y4einLciCjt/+0x6/4AG80fjay2Q==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/describe-ref": "3.16.5", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@lerna/command": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz", + "integrity": "sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/package-graph": "3.18.5", + "@lerna/project": "3.21.0", + "@lerna/validation-error": "3.13.0", + "@lerna/write-log-file": "3.13.0", + "clone-deep": "^4.0.1", + "dedent": "^0.7.0", + "execa": "^1.0.0", + "is-ci": "^2.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/conventional-commits": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz", + "integrity": "sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA==", + "dev": true, + "requires": { + "@lerna/validation-error": "3.13.0", + "conventional-changelog-angular": "^5.0.3", + "conventional-changelog-core": "^3.1.6", + "conventional-recommended-bump": "^5.0.0", + "fs-extra": "^8.1.0", + "get-stream": "^4.0.0", + "lodash.template": "^4.5.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "pify": "^4.0.1", + "semver": "^6.2.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "@lerna/create": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz", + "integrity": "sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw==", + "dev": true, + "requires": { + "@evocateur/pacote": "^9.6.3", + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.21.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/validation-error": "3.13.0", + "camelcase": "^5.0.0", + "dedent": "^0.7.0", + "fs-extra": "^8.1.0", + "globby": "^9.2.0", + "init-package-json": "^1.10.3", + "npm-package-arg": "^6.1.0", + "p-reduce": "^1.0.0", + "pify": "^4.0.1", + "semver": "^6.2.0", + "slash": "^2.0.0", + "validate-npm-package-license": "^3.0.3", + "validate-npm-package-name": "^3.0.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@lerna/create-symlink": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.16.2.tgz", + "integrity": "sha512-pzXIJp6av15P325sgiIRpsPXLFmkisLhMBCy4764d+7yjf2bzrJ4gkWVMhsv4AdF0NN3OyZ5jjzzTtLNqfR+Jw==", + "dev": true, + "requires": { + "@zkochan/cmd-shim": "^3.1.0", + "fs-extra": "^8.1.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/describe-ref": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz", + "integrity": "sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "npmlog": "^4.1.2" + } + }, + "@lerna/diff": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz", + "integrity": "sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.21.0", + "@lerna/validation-error": "3.13.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/exec": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz", + "integrity": "sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/profiler": "3.20.0", + "@lerna/run-topologically": "3.18.5", + "@lerna/validation-error": "3.13.0", + "p-map": "^2.1.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/filter-options": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.20.0.tgz", + "integrity": "sha512-bmcHtvxn7SIl/R9gpiNMVG7yjx7WyT0HSGw34YVZ9B+3xF/83N3r5Rgtjh4hheLZ+Q91Or0Jyu5O3Nr+AwZe2g==", + "dev": true, + "requires": { + "@lerna/collect-updates": "3.20.0", + "@lerna/filter-packages": "3.18.0", + "dedent": "^0.7.0", + "figgy-pudding": "^3.5.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/filter-packages": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz", + "integrity": "sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ==", + "dev": true, + "requires": { + "@lerna/validation-error": "3.13.0", + "multimatch": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/get-npm-exec-opts": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz", + "integrity": "sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw==", + "dev": true, + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/get-packed": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.16.0.tgz", + "integrity": "sha512-AjsFiaJzo1GCPnJUJZiTW6J1EihrPkc2y3nMu6m3uWFxoleklsSCyImumzVZJssxMi3CPpztj8LmADLedl9kXw==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "ssri": "^6.0.1", + "tar": "^4.4.8" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + } + } + }, + "@lerna/github-client": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz", + "integrity": "sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@octokit/plugin-enterprise-rest": "^6.0.1", + "@octokit/rest": "^16.28.4", + "git-url-parse": "^11.1.2", + "npmlog": "^4.1.2" + } + }, + "@lerna/gitlab-client": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-3.15.0.tgz", + "integrity": "sha512-OsBvRSejHXUBMgwWQqNoioB8sgzL/Pf1pOUhHKtkiMl6aAWjklaaq5HPMvTIsZPfS6DJ9L5OK2GGZuooP/5c8Q==", + "dev": true, + "requires": { + "node-fetch": "^2.5.0", + "npmlog": "^4.1.2", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + } + } + }, + "@lerna/global-options": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz", + "integrity": "sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==", + "dev": true + }, + "@lerna/has-npm-version": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz", + "integrity": "sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "semver": "^6.2.0" + } + }, + "@lerna/import": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz", + "integrity": "sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.21.0", + "@lerna/prompt": "3.18.5", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/validation-error": "3.13.0", + "dedent": "^0.7.0", + "fs-extra": "^8.1.0", + "p-map-series": "^1.0.0" + } + }, + "@lerna/info": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz", + "integrity": "sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/output": "3.13.0", + "envinfo": "^7.3.1" + } + }, + "@lerna/init": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz", + "integrity": "sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/command": "3.21.0", + "fs-extra": "^8.1.0", + "p-map": "^2.1.0", + "write-json-file": "^3.2.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/link": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz", + "integrity": "sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/package-graph": "3.18.5", + "@lerna/symlink-dependencies": "3.17.0", + "p-map": "^2.1.0", + "slash": "^2.0.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@lerna/list": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz", + "integrity": "sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/listable": "3.18.5", + "@lerna/output": "3.13.0" + } + }, + "@lerna/listable": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.5.tgz", + "integrity": "sha512-Sdr3pVyaEv5A7ZkGGYR7zN+tTl2iDcinryBPvtuv20VJrXBE8wYcOks1edBTcOWsPjCE/rMP4bo1pseyk3UTsg==", + "dev": true, + "requires": { + "@lerna/query-graph": "3.18.5", + "chalk": "^2.3.1", + "columnify": "^1.5.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@lerna/log-packed": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.16.0.tgz", + "integrity": "sha512-Fp+McSNBV/P2mnLUYTaSlG8GSmpXM7krKWcllqElGxvAqv6chk2K3c2k80MeVB4WvJ9tRjUUf+i7HUTiQ9/ckQ==", + "dev": true, + "requires": { + "byte-size": "^5.0.1", + "columnify": "^1.5.4", + "has-unicode": "^2.0.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-conf": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.16.0.tgz", + "integrity": "sha512-HbO3DUrTkCAn2iQ9+FF/eisDpWY5POQAOF1m7q//CZjdC2HSW3UYbKEGsSisFxSfaF9Z4jtrV+F/wX6qWs3CuA==", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^4.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "@lerna/npm-dist-tag": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.5.tgz", + "integrity": "sha512-xw0HDoIG6HreVsJND9/dGls1c+lf6vhu7yJoo56Sz5bvncTloYGLUppIfDHQr4ZvmPCK8rsh0euCVh2giPxzKQ==", + "dev": true, + "requires": { + "@evocateur/npm-registry-fetch": "^4.0.0", + "@lerna/otplease": "3.18.5", + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-install": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz", + "integrity": "sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/get-npm-exec-opts": "3.13.0", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "signal-exit": "^3.0.2", + "write-pkg": "^3.1.0" + } + }, + "@lerna/npm-publish": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.18.5.tgz", + "integrity": "sha512-3etLT9+2L8JAx5F8uf7qp6iAtOLSMj+ZYWY6oUgozPi/uLqU0/gsMsEXh3F0+YVW33q0M61RpduBoAlOOZnaTg==", + "dev": true, + "requires": { + "@evocateur/libnpmpublish": "^1.2.2", + "@lerna/otplease": "3.18.5", + "@lerna/run-lifecycle": "3.16.2", + "figgy-pudding": "^3.5.1", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "pify": "^4.0.1", + "read-package-json": "^2.0.13" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } + } + }, + "@lerna/npm-run-script": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz", + "integrity": "sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "@lerna/get-npm-exec-opts": "3.13.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/otplease": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/otplease/-/otplease-3.18.5.tgz", + "integrity": "sha512-S+SldXAbcXTEDhzdxYLU0ZBKuYyURP/ND2/dK6IpKgLxQYh/z4ScljPDMyKymmEvgiEJmBsPZAAPfmNPEzxjog==", + "dev": true, + "requires": { + "@lerna/prompt": "3.18.5", + "figgy-pudding": "^3.5.1" + } + }, + "@lerna/output": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz", + "integrity": "sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg==", + "dev": true, + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/pack-directory": { + "version": "3.16.4", + "resolved": "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.16.4.tgz", + "integrity": "sha512-uxSF0HZeGyKaaVHz5FroDY9A5NDDiCibrbYR6+khmrhZtY0Bgn6hWq8Gswl9iIlymA+VzCbshWIMX4o2O8C8ng==", + "dev": true, + "requires": { + "@lerna/get-packed": "3.16.0", + "@lerna/package": "3.16.0", + "@lerna/run-lifecycle": "3.16.2", + "figgy-pudding": "^3.5.1", + "npm-packlist": "^1.4.4", + "npmlog": "^4.1.2", + "tar": "^4.4.10", + "temp-write": "^3.4.0" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + } + } + }, + "@lerna/package": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz", + "integrity": "sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw==", + "dev": true, + "requires": { + "load-json-file": "^5.3.0", + "npm-package-arg": "^6.1.0", + "write-pkg": "^3.1.0" + }, + "dependencies": { + "load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + } + } + }, + "@lerna/package-graph": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz", + "integrity": "sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA==", + "dev": true, + "requires": { + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/validation-error": "3.13.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "semver": "^6.2.0" + } + }, + "@lerna/prerelease-id-from-version": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz", + "integrity": "sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA==", + "dev": true, + "requires": { + "semver": "^6.2.0" + } + }, + "@lerna/profiler": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz", + "integrity": "sha512-bh8hKxAlm6yu8WEOvbLENm42i2v9SsR4WbrCWSbsmOElx3foRnMlYk7NkGECa+U5c3K4C6GeBbwgqs54PP7Ljg==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "fs-extra": "^8.1.0", + "npmlog": "^4.1.2", + "upath": "^1.2.0" + } + }, + "@lerna/project": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz", + "integrity": "sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A==", + "dev": true, + "requires": { + "@lerna/package": "3.16.0", + "@lerna/validation-error": "3.13.0", + "cosmiconfig": "^5.1.0", + "dedent": "^0.7.0", + "dot-prop": "^4.2.0", + "glob-parent": "^5.0.0", + "globby": "^9.2.0", + "load-json-file": "^5.3.0", + "npmlog": "^4.1.2", + "p-map": "^2.1.0", + "resolve-from": "^4.0.0", + "write-json-file": "^3.2.0" + }, + "dependencies": { + "load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + } + } + }, + "@lerna/prompt": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz", + "integrity": "sha512-rkKj4nm1twSbBEb69+Em/2jAERK8htUuV8/xSjN0NPC+6UjzAwY52/x9n5cfmpa9lyKf/uItp7chCI7eDmNTKQ==", + "dev": true, + "requires": { + "inquirer": "^6.2.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/publish": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.1.tgz", + "integrity": "sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw==", + "dev": true, + "requires": { + "@evocateur/libnpmaccess": "^3.1.2", + "@evocateur/npm-registry-fetch": "^4.0.0", + "@evocateur/pacote": "^9.6.3", + "@lerna/check-working-tree": "3.16.5", + "@lerna/child-process": "3.16.5", + "@lerna/collect-updates": "3.20.0", + "@lerna/command": "3.21.0", + "@lerna/describe-ref": "3.16.5", + "@lerna/log-packed": "3.16.0", + "@lerna/npm-conf": "3.16.0", + "@lerna/npm-dist-tag": "3.18.5", + "@lerna/npm-publish": "3.18.5", + "@lerna/otplease": "3.18.5", + "@lerna/output": "3.13.0", + "@lerna/pack-directory": "3.16.4", + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/prompt": "3.18.5", + "@lerna/pulse-till-done": "3.13.0", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.5", + "@lerna/validation-error": "3.13.0", + "@lerna/version": "3.22.1", + "figgy-pudding": "^3.5.1", + "fs-extra": "^8.1.0", + "npm-package-arg": "^6.1.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-pipe": "^1.2.0", + "semver": "^6.2.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/pulse-till-done": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz", + "integrity": "sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA==", + "dev": true, + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/query-graph": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.5.tgz", + "integrity": "sha512-50Lf4uuMpMWvJ306be3oQDHrWV42nai9gbIVByPBYJuVW8dT8O8pA3EzitNYBUdLL9/qEVbrR0ry1HD7EXwtRA==", + "dev": true, + "requires": { + "@lerna/package-graph": "3.18.5", + "figgy-pudding": "^3.5.1" + } + }, + "@lerna/resolve-symlink": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.16.0.tgz", + "integrity": "sha512-Ibj5e7njVHNJ/NOqT4HlEgPFPtPLWsO7iu59AM5bJDcAJcR96mLZ7KGVIsS2tvaO7akMEJvt2P+ErwCdloG3jQ==", + "dev": true, + "requires": { + "fs-extra": "^8.1.0", + "npmlog": "^4.1.2", + "read-cmd-shim": "^1.0.1" + } + }, + "@lerna/rimraf-dir": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz", + "integrity": "sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA==", + "dev": true, + "requires": { + "@lerna/child-process": "3.16.5", + "npmlog": "^4.1.2", + "path-exists": "^3.0.0", + "rimraf": "^2.6.2" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "@lerna/run": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz", + "integrity": "sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q==", + "dev": true, + "requires": { + "@lerna/command": "3.21.0", + "@lerna/filter-options": "3.20.0", + "@lerna/npm-run-script": "3.16.5", + "@lerna/output": "3.13.0", + "@lerna/profiler": "3.20.0", + "@lerna/run-topologically": "3.18.5", + "@lerna/timer": "3.13.0", + "@lerna/validation-error": "3.13.0", + "p-map": "^2.1.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/run-lifecycle": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.16.2.tgz", + "integrity": "sha512-RqFoznE8rDpyyF0rOJy3+KjZCeTkO8y/OB9orPauR7G2xQ7PTdCpgo7EO6ZNdz3Al+k1BydClZz/j78gNCmL2A==", + "dev": true, + "requires": { + "@lerna/npm-conf": "3.16.0", + "figgy-pudding": "^3.5.1", + "npm-lifecycle": "^3.1.2", + "npmlog": "^4.1.2" + } + }, + "@lerna/run-topologically": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.5.tgz", + "integrity": "sha512-6N1I+6wf4hLOnPW+XDZqwufyIQ6gqoPfHZFkfWlvTQ+Ue7CuF8qIVQ1Eddw5HKQMkxqN10thKOFfq/9NQZ4NUg==", + "dev": true, + "requires": { + "@lerna/query-graph": "3.18.5", + "figgy-pudding": "^3.5.1", + "p-queue": "^4.0.0" + } + }, + "@lerna/symlink-binary": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz", + "integrity": "sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ==", + "dev": true, + "requires": { + "@lerna/create-symlink": "3.16.2", + "@lerna/package": "3.16.0", + "fs-extra": "^8.1.0", + "p-map": "^2.1.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/symlink-dependencies": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz", + "integrity": "sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q==", + "dev": true, + "requires": { + "@lerna/create-symlink": "3.16.2", + "@lerna/resolve-symlink": "3.16.0", + "@lerna/symlink-binary": "3.17.0", + "fs-extra": "^8.1.0", + "p-finally": "^1.0.0", + "p-map": "^2.1.0", + "p-map-series": "^1.0.0" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "@lerna/timer": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz", + "integrity": "sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw==", + "dev": true + }, + "@lerna/validation-error": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz", + "integrity": "sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA==", + "dev": true, + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/version": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz", + "integrity": "sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g==", + "dev": true, + "requires": { + "@lerna/check-working-tree": "3.16.5", + "@lerna/child-process": "3.16.5", + "@lerna/collect-updates": "3.20.0", + "@lerna/command": "3.21.0", + "@lerna/conventional-commits": "3.22.0", + "@lerna/github-client": "3.22.0", + "@lerna/gitlab-client": "3.15.0", + "@lerna/output": "3.13.0", + "@lerna/prerelease-id-from-version": "3.16.0", + "@lerna/prompt": "3.18.5", + "@lerna/run-lifecycle": "3.16.2", + "@lerna/run-topologically": "3.18.5", + "@lerna/validation-error": "3.13.0", + "chalk": "^2.3.1", + "dedent": "^0.7.0", + "load-json-file": "^5.3.0", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "p-map": "^2.1.0", + "p-pipe": "^1.2.0", + "p-reduce": "^1.0.0", + "p-waterfall": "^1.0.0", + "semver": "^6.2.0", + "slash": "^2.0.0", + "temp-write": "^3.4.0", + "write-json-file": "^3.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + } + } + }, + "@lerna/write-log-file": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz", + "integrity": "sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A==", + "dev": true, + "requires": { + "npmlog": "^4.1.2", + "write-file-atomic": "^2.3.0" + }, + "dependencies": { + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "requires": { + "mkdirp": "^1.0.4" + } + }, + "@octokit/auth-token": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", + "integrity": "sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ==", + "dev": true, + "requires": { + "@octokit/types": "^5.0.0" + } + }, + "@octokit/endpoint": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.5.tgz", + "integrity": "sha512-70K5u6zd45ItOny6aHQAsea8HHQjlQq85yqOMe+Aj8dkhN2qSJ9T+Q3YjUjEYfPRBcuUWNgMn62DQnP/4LAIiQ==", + "dev": true, + "requires": { + "@octokit/types": "^5.0.0", + "is-plain-object": "^4.0.0", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-4.1.1.tgz", + "integrity": "sha512-5Aw8LLVsDlZsETVMhoMXzqsXwQqr/0vlnBYzIXJbYo2F4yYlhLHs+Ez7Bod7IIQKWkJbJfxrWD7pA1Dw1TKrwA==", + "dev": true + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + } + } + }, + "@octokit/plugin-enterprise-rest": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz", + "integrity": "sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==", + "dev": true + }, + "@octokit/plugin-paginate-rest": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.1" + }, + "dependencies": { + "@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "requires": { + "@types/node": ">= 8" + } + } + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz", + "integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==", + "dev": true + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.1", + "deprecation": "^2.3.1" + }, + "dependencies": { + "@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "requires": { + "@types/node": ">= 8" + } + } + } + }, + "@octokit/request": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.7.tgz", + "integrity": "sha512-FN22xUDP0i0uF38YMbOfx6TotpcENP5W8yJM1e/LieGXn6IoRxDMnBf7tx5RKSW4xuUZ/1P04NFZy5iY3Rax1A==", + "dev": true, + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.0.0", + "@octokit/types": "^5.0.0", + "deprecation": "^2.0.0", + "is-plain-object": "^4.0.0", + "node-fetch": "^2.3.0", + "once": "^1.4.0", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/request-error": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz", + "integrity": "sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==", + "dev": true, + "requires": { + "@octokit/types": "^5.0.1", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "is-plain-object": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-4.1.1.tgz", + "integrity": "sha512-5Aw8LLVsDlZsETVMhoMXzqsXwQqr/0vlnBYzIXJbYo2F4yYlhLHs+Ez7Bod7IIQKWkJbJfxrWD7pA1Dw1TKrwA==", + "dev": true + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + } + } + }, + "@octokit/request-error": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "dependencies": { + "@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "requires": { + "@types/node": ">= 8" + } + } + } + }, + "@octokit/rest": { + "version": "16.43.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", + "dev": true, + "requires": { + "@octokit/auth-token": "^2.4.0", + "@octokit/plugin-paginate-rest": "^1.1.1", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "2.4.0", + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "@octokit/types": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.4.1.tgz", + "integrity": "sha512-OlMlSySBJoJ6uozkr/i03nO5dlYQyE05vmQNZhAh9MyO4DPBP88QlwsDVLmVjIMFssvIZB6WO0ctIGMRG+xsJQ==", + "dev": true, + "requires": { + "@types/node": ">= 8" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "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": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/babel__core": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz", + "integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", + "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==" + }, + "@types/cookies": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.4.tgz", + "integrity": "sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/cors": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.7.tgz", + "integrity": "sha512-sOdDRU3oRS7LBNTIqwDkPJyq0lpHYcbMTt0TrjzsXbk/e37hcLTH6eZX7CdbDeN0yJJvzw9hFBZkbtCSbk/jAQ==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/express": { + "version": "4.17.8", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz", + "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz", + "integrity": "sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/fs-capacitor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz", + "integrity": "sha512-FKVPOCFbhCvZxpVAMhdBdTfVfXUpsh15wFHgqOKxh9N9vzWZVuWCSijZ5T4U34XYNnuj2oduh6xcs1i+LPI+BQ==", + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", + "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/graphql-upload": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.4.tgz", + "integrity": "sha512-0TRyJD2o8vbkmJF8InppFcPVcXKk+Rvlg/xvpHBIndSJYpmDWfmtx/ZAtl4f3jR2vfarpTqYgj8MZuJssSoU7Q==", + "requires": { + "@types/express": "*", + "@types/fs-capacitor": "*", + "@types/koa": "*", + "graphql": "^15.3.0" + }, + "dependencies": { + "graphql": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", + "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==" + } + } + }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" + }, + "@types/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==" + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz", + "integrity": "sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + }, + "@types/koa": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.4.tgz", + "integrity": "sha512-Etqs0kdqbuAsNr5k6mlZQelpZKVwMu9WPRHVVTLnceZlhr0pYmblRNJbCgoCMzKWWePldydU0AYEOX4Q9fnGUQ==", + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "requires": { + "@types/koa": "*" + } + }, + "@types/lodash": { + "version": "4.14.161", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", + "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==", + "dev": true + }, + "@types/lodash.xorby": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@types/lodash.xorby/-/lodash.xorby-4.7.6.tgz", + "integrity": "sha512-PtlgNvKxKD65DCbm8OPokdYnm3kIrBYTXc20vbsF0QX+il1c9mmb4KxKE3U0ty9w8AoA+RxyT7hxHd/doFDcGg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/loglevel": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/loglevel/-/loglevel-1.5.4.tgz", + "integrity": "sha512-8dx4ckP0vndJeN+iKZwdGiapLqFjVQ3JLOt92uqK0C63acs5NcPLbUOpfXCJkKVRjZLBQjw8NIGNBSsnatFnFQ==", + "dev": true + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, + "@types/nock": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", + "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "8.10.62", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.62.tgz", + "integrity": "sha512-76fupxOYVxk36kb7O/6KtrAPZ9jnSK3+qisAX4tQMEuGNdlvl7ycwatlHqjoE6jHfVtXFM3pCrCixZOidc5cuw==", + "dev": true + }, + "@types/node-fetch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.3.2.tgz", + "integrity": "sha512-yW0EOebSsQme9yKu09XbdDfle4/SmWZMK4dfteWcSLCYNQQcF+YOv0kIrvm+9pO11/ghA4E6A+RNQqvYj4Nr3A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", + "dev": true + }, + "@types/qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", + "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/ws": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz", + "integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==", + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + } + } + }, + "@types/yargs": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", + "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" + }, + "@wry/equality": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", + "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", + "requires": { + "tslib": "^1.9.3" + } + }, + "@zkochan/cmd-shim": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz", + "integrity": "sha512-o8l0+x7C7sMZU3v9GuJIAU10qQLtwR1dtRQIOmlNMtyaqhmpXOzx1HWiYoWfmmf9HHZoAkXpc9TM9PQYF9d4Jg==", + "dev": true, + "requires": { + "is-windows": "^1.0.0", + "mkdirp-promise": "^5.0.1", + "mz": "^2.5.0" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abab": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.3.tgz", + "integrity": "sha512-wn8fw19xKZwdGPO47jivonaHRTd+nGOMP1z11sgGeQzDy2xd5FG0R67dIMcKHDE2cJ5y+YXV30XVGUBPRSY7Hg==", + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apollo-cache-control": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.1.tgz", + "integrity": "sha512-6iHa8TkcKt4rx5SKRzDNjUIpCQX+7/FlZwD7vRh9JDnM4VH8SWhpj8fUR3CiEY8Kuc4ChXnOY8bCcMju5KPnIQ==", + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.9.1" + } + }, + "apollo-datasource": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", + "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", + "requires": { + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-engine-reporting": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.3.0.tgz", + "integrity": "sha512-SbcPLFuUZcRqDEZ6mSs8uHM9Ftr8yyt2IEu0JA8c3LNBmYXSLM7MHqFe80SVcosYSTBgtMz8mLJO8orhYoSYZw==", + "requires": { + "apollo-engine-reporting-protobuf": "^0.5.2", + "apollo-graphql": "^0.5.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-plugin-base": "^0.9.1", + "apollo-server-types": "^0.5.1", + "async-retry": "^1.2.1", + "uuid": "^8.0.0" + }, + "dependencies": { + "apollo-graphql": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.5.0.tgz", + "integrity": "sha512-YSdF/BKPbsnQpxWpmCE53pBJX44aaoif31Y22I/qKpB6ZSGzYijV5YBoCL5Q15H2oA/v/02Oazh9lbp4ek3eig==", + "requires": { + "apollo-env": "^0.6.5", + "lodash.sortby": "^4.7.0" + } + } + } + }, + "apollo-engine-reporting-protobuf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.2.tgz", + "integrity": "sha512-4wm9FR3B7UvJxcK/69rOiS5CAJPEYKufeRWb257ZLfX7NGFTMqvbc1hu4q8Ch7swB26rTpkzfsftLED9DqH9qg==", + "requires": { + "@apollo/protobufjs": "^1.0.3" + } + }, + "apollo-env": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", + "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", + "requires": { + "@types/node-fetch": "2.5.7", + "core-js": "^3.0.1", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + }, + "dependencies": { + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==" + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } + }, + "apollo-federation-integration-testsuite": { + "version": "file:federation-integration-testsuite-js", + "requires": { + "apollo-graphql": "^0.6.0", + "graphql-tag": "^2.10.4" + } + }, + "apollo-graphql": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", + "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", + "requires": { + "apollo-env": "^0.6.5", + "lodash.sortby": "^4.7.0" + } + }, + "apollo-link": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", + "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.21" + } + }, + "apollo-link-http": { + "version": "1.5.17", + "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", + "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "apollo-link-http-common": "^0.2.16", + "tslib": "^1.9.3" + } + }, + "apollo-link-http-common": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", + "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.14", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + } + }, + "apollo-server": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/apollo-server/-/apollo-server-2.17.0.tgz", + "integrity": "sha512-vVMu+VqjmsB6yk5iNTb/AXM6EJGd2uwzrcTAbwqvGI7GqCYDRZlGBwrQCjOU/jT/EPWdNRWks/qhJYiQMeVXSg==", + "dev": true, + "requires": { + "apollo-server-core": "^2.17.0", + "apollo-server-express": "^2.17.0", + "express": "^4.0.0", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0" + } + }, + "apollo-server-caching": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", + "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-core": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.17.0.tgz", + "integrity": "sha512-rjAkBbKSrGLDfg/g5bohnPlQahmkAxgEBuMDVsoF3aa+RaEPXPUMYrLbOxntl0LWeLbPiMa/IyFF43dvlGqV7w==", + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "@apollographql/graphql-playground-html": "1.6.26", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^7.0.0", + "apollo-cache-control": "^0.11.1", + "apollo-datasource": "^0.7.2", + "apollo-engine-reporting": "^2.3.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-plugin-base": "^0.9.1", + "apollo-server-types": "^0.5.1", + "apollo-tracing": "^0.11.2", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.12.4", + "graphql-tag": "^2.9.2", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "loglevel": "^1.6.7", + "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.11", + "ws": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", + "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } + }, + "apollo-server-errors": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz", + "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==" + }, + "apollo-server-express": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.17.0.tgz", + "integrity": "sha512-PonpWOuM1DH3Cz0bu56Tusr3GXOnectC6AD/gy2GXK0v84E7tKTuxEY3SgsgxhvfvvhfwJbXTyIogL/wezqnCw==", + "dev": true, + "requires": { + "@apollographql/graphql-playground-html": "1.6.26", + "@types/accepts": "^1.3.5", + "@types/body-parser": "1.19.0", + "@types/cors": "^2.8.4", + "@types/express": "4.17.7", + "accepts": "^1.3.5", + "apollo-server-core": "^2.17.0", + "apollo-server-types": "^0.5.1", + "body-parser": "^1.18.3", + "cors": "^2.8.4", + "express": "^4.17.1", + "graphql-subscriptions": "^1.0.0", + "graphql-tools": "^4.0.0", + "parseurl": "^1.3.2", + "subscriptions-transport-ws": "^0.9.16", + "type-is": "^1.6.16" + }, + "dependencies": { + "@types/express": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", + "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + } + } + }, + "apollo-server-plugin-base": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.9.1.tgz", + "integrity": "sha512-kvrX4Z3FdpjrZdHkyl5iY2A1Wvp4b6KQp00DeZqss7GyyKNUBKr80/7RQgBLEw7EWM7WB19j459xM/TjvW0FKQ==", + "requires": { + "apollo-server-types": "^0.5.1" + } + }, + "apollo-server-testing": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/apollo-server-testing/-/apollo-server-testing-2.17.0.tgz", + "integrity": "sha512-pb6ijqDbKUkeaJKP6P3sgT24X/1yErbBfobquyzOpkgyRCc93MeAY4xG3woA1/WZHQm0RN8xtsYelEKz05i14g==", + "dev": true, + "requires": { + "apollo-server-core": "^2.17.0" + } + }, + "apollo-server-types": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.5.1.tgz", + "integrity": "sha512-my2cPw+DAb2qVnIuBcsRKGyS28uIc2vjFxa1NpRoJZe9gK0BWUBk7wzXnIzWy3HZ5Er11e/40MPTUesNfMYNVA==", + "requires": { + "apollo-engine-reporting-protobuf": "^0.5.2", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-tracing": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.11.2.tgz", + "integrity": "sha512-QjmRd2ozGD+PfmF6U9w/w6jrclYSBNczN6Bzppr8qA5somEGl5pqdprIZYL28H0IapZiutA3x6p6ZVF/cVX8wA==", + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.9.1" + } + }, + "apollo-utilities": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", + "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz", + "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "async-retry": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", + "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", + "requires": { + "retry": "0.12.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "atob-lite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", + "dev": true + }, + "babel-jest": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", + "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", + "dev": true, + "requires": { + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", + "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", + "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", + "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^25.5.0", + "babel-preset-current-node-syntax": "^0.1.2" + } + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "before-after-hook": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", + "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", + "dev": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "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" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bunyan": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.14.tgz", + "integrity": "sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg==", + "dev": true, + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "dev": true + }, + "byte-size": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz", + "integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codecov": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.2.tgz", + "integrity": "sha512-fmCjAkTese29DUX3GMIi4EaKGflHa4K51EoMc29g8fBHawdk/+KEq5CWOeXLdd9+AT7o1wO4DIpp/Z1KCqCz1g==", + "dev": true, + "requires": { + "argv": "0.0.2", + "ignore-walk": "3.0.3", + "js-yaml": "3.13.1", + "teeny-request": "6.0.1", + "urlgrey": "0.4.4" + } + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "dev": true, + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "columnify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "dev": true, + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "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==" + }, + "compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + } + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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 + }, + "conventional-changelog-angular": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz", + "integrity": "sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-core": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz", + "integrity": "sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ==", + "dev": true, + "requires": { + "conventional-changelog-writer": "^4.0.6", + "conventional-commits-parser": "^3.0.3", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "2.0.0", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^2.0.3", + "lodash": "^4.2.1", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-changelog-preset-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", + "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", + "dev": true + }, + "conventional-changelog-writer": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.17.tgz", + "integrity": "sha512-IKQuK3bib/n032KWaSb8YlBFds+aLmzENtnKtxJy3+HqDq5kohu3g/UdNbIHeJWygfnEbZjnCKFxAW0y7ArZAw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "conventional-commits-filter": "^2.0.6", + "dateformat": "^3.0.0", + "handlebars": "^4.7.6", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^7.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^3.0.0" + }, + "dependencies": { + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-commits-filter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.6.tgz", + "integrity": "sha512-4g+sw8+KA50/Qwzfr0hL5k5NWxqtrOVw4DDk3/h6L85a9Gz0/Eqp3oP+CWCNfesBvZZZEFHF7OTEbRe+yYSyKw==", + "dev": true, + "requires": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz", + "integrity": "sha512-RSo5S0WIwXZiRxUGTPuYFbqvrR4vpJ1BDdTlthFgvHt5kEdnd1+pdvwWphWn57/oIl4V72NMmOocFqqJ8mFFhA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.1", + "lodash": "^4.17.15", + "meow": "^7.0.0", + "split2": "^2.0.0", + "through2": "^3.0.0", + "trim-off-newlines": "^1.0.0" + }, + "dependencies": { + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "conventional-recommended-bump": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz", + "integrity": "sha512-RVdt0elRcCxL90IrNP0fYCpq1uGt2MALko0eyeQ+zQuDVWtMGAy9ng6yYn3kax42lCj9+XBxQ8ZN6S9bdKxDhQ==", + "dev": true, + "requires": { + "concat-stream": "^2.0.0", + "conventional-changelog-preset-loader": "^2.1.1", + "conventional-commits-filter": "^2.0.2", + "conventional-commits-parser": "^3.0.3", + "git-raw-commits": "2.0.0", + "git-semver-tags": "^2.0.3", + "meow": "^4.0.0", + "q": "^1.5.1" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "date-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==", + "dev": true + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "deprecated-decorator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", + "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "requires": { + "iconv-lite": "^0.6.2" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "envinfo": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.3.tgz", + "integrity": "sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==", + "dev": true + }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expect": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", + "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "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": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==", + "dev": true + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "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": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-capacitor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-2.0.4.tgz", + "integrity": "sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "genfun": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", + "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + } + } + }, + "get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "gherkin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-5.1.0.tgz", + "integrity": "sha1-aEu7A63STq9731RPWAM+so+zxtU=", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "git-semver-tags": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.3.tgz", + "integrity": "sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA==", + "dev": true, + "requires": { + "meow": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + } + } + }, + "git-up": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.2.tgz", + "integrity": "sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^5.0.0" + } + }, + "git-url-parse": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.2.0.tgz", + "integrity": "sha512-KPoHZg8v+plarZvto4ruIzzJLFQoRx+sUs5DQSr07By9IBKguVd+e6jwrFR6/TP6xrCJlNV1tPqLO1aREc7O2g==", + "dev": true, + "requires": { + "git-up": "^4.0.0" + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "graphql": { + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz", + "integrity": "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==", + "dev": true, + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-extensions": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.4.tgz", + "integrity": "sha512-GnR4LiWk3s2bGOqIh6V1JgnSXw2RCH4NOgbCFEWvB6JqWHXTlXnLZ8bRSkCiD4pltv7RHUPWqN/sGh8R6Ae/ag==", + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "apollo-server-env": "^2.4.5", + "apollo-server-types": "^0.5.1" + } + }, + "graphql-subscriptions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", + "integrity": "sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA==", + "dev": true, + "requires": { + "iterall": "^1.2.1" + } + }, + "graphql-tag": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz", + "integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==" + }, + "graphql-tools": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz", + "integrity": "sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg==", + "requires": { + "apollo-link": "^1.2.14", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "graphql-upload": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.1.0.tgz", + "integrity": "sha512-U2OiDI5VxYmzRKw0Z2dmfk0zkqMRaecH9Smh1U277gVgVe9Qn+18xqf4skwr4YJszGIh7iQDZ57+5ygOK9sM/Q==", + "requires": { + "busboy": "^0.3.1", + "fs-capacitor": "^2.0.4", + "http-errors": "^1.7.3", + "object-path": "^0.11.4" + } + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "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": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "init-package-json": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", + "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", + "dev": true, + "requires": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "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-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true, + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-ssh": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz", + "integrity": "sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ==", + "dev": true, + "requires": { + "protocols": "^1.1.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, + "jest": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", + "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", + "dev": true, + "requires": { + "@jest/core": "^25.5.4", + "import-local": "^3.0.2", + "jest-cli": "^25.5.4" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-cli": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", + "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", + "dev": true, + "requires": { + "@jest/core": "^25.5.4", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^25.5.4", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "prompts": "^2.0.1", + "realpath-native": "^2.0.0", + "yargs": "^15.3.1" + } + } + } + }, + "jest-changed-files": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", + "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "execa": "^3.2.0", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } + }, + "jest-config": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", + "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^25.5.4", + "@jest/types": "^25.5.0", + "babel-jest": "^25.5.1", + "chalk": "^3.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^25.5.0", + "jest-environment-node": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-jasmine2": "^25.5.4", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "micromatch": "^4.0.2", + "pretty-format": "^25.5.0", + "realpath-native": "^2.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-cucumber": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/jest-cucumber/-/jest-cucumber-2.0.13.tgz", + "integrity": "sha512-GBlS6OtUIU1E7SBYryN6m4pEl4jy/Fga6dpvXt5nehg3ItnxTRINK6CcYwJq9ToJMV0pkdHd0AhvBwBOiJ19Zw==", + "dev": true, + "requires": { + "@types/jest": "^24.0.7", + "@types/node": "^11.9.4", + "callsites": "^3.0.0", + "gherkin": "^5.0.0", + "jest": "^24.1.0" + }, + "dependencies": { + "@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "requires": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + } + }, + "@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + } + }, + "@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + } + }, + "@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + } + }, + "@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + } + }, + "@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + } + }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "dev": true, + "requires": { + "jest-diff": "^24.3.0" + } + }, + "@types/node": { + "version": "11.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.21.tgz", + "integrity": "sha512-pYC/tp6UctD/laukjIoO48Owmf4OacwEKaDErnlarkatott0j3B5wp9q2M0zn/XgZUpebuFeKMX4UFf578/+Vw==", + "dev": true + }, + "@types/yargs": { + "version": "13.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", + "integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "requires": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + } + }, + "babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "requires": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "dependencies": { + "jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "requires": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + } + } + } + }, + "jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + } + }, + "jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "requires": { + "detect-newline": "^2.1.0" + } + }, + "jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + } + }, + "jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "requires": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + } + }, + "jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "requires": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0" + } + }, + "jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true + }, + "jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + } + }, + "jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + } + }, + "jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "requires": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + } + }, + "jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true + }, + "jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + } + }, + "jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "requires": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + } + }, + "jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + } + }, + "jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "requires": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + } + }, + "jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "node-notifier": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", + "dev": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-docblock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", + "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", + "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-environment-jsdom": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", + "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "jsdom": "^15.2.1" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-environment-node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", + "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jest-haste-map": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", + "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "@types/graceful-fs": "^4.1.2", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-serializer": "^25.5.0", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-jasmine2": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", + "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^25.5.0", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "co": "^4.6.0", + "expect": "^25.5.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^25.5.0", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-junit": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-10.0.0.tgz", + "integrity": "sha512-dbOVRyxHprdSpwSAR9/YshLwmnwf+RSl5hf0kCGlhAcEeZY9aRqo4oNmaT0tLC16Zy9D0zekDjWkjHGjXlglaQ==", + "dev": true, + "requires": { + "jest-validate": "^24.9.0", + "mkdirp": "^0.5.1", + "strip-ansi": "^5.2.0", + "uuid": "^3.3.3", + "xml": "^1.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "13.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", + "integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "jest-leak-detector": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", + "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", + "dev": true, + "requires": { + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-mock": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", + "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "dev": true + }, + "jest-resolve": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", + "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "browser-resolve": "^1.11.3", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.1", + "read-pkg-up": "^7.0.1", + "realpath-native": "^2.0.0", + "resolve": "^1.17.0", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", + "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-snapshot": "^25.5.1" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-runner": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", + "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-docblock": "^25.3.0", + "jest-haste-map": "^25.5.1", + "jest-jasmine2": "^25.5.4", + "jest-leak-detector": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "jest-runtime": "^25.5.4", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-runtime": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", + "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/globals": "^25.5.2", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.3.1" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-serializer": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", + "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", + "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/prettier": "^1.19.0", + "chalk": "^3.0.0", + "expect": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "make-dir": "^3.0.0", + "natural-compare": "^1.4.0", + "pretty-format": "^25.5.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", + "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-validate": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", + "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "leven": "^3.1.0", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + } + } + }, + "jest-watcher": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", + "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", + "dev": true, + "requires": { + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "jest-util": "^25.5.0", + "string-length": "^3.1.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "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": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "lerna": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz", + "integrity": "sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg==", + "dev": true, + "requires": { + "@lerna/add": "3.21.0", + "@lerna/bootstrap": "3.21.0", + "@lerna/changed": "3.21.0", + "@lerna/clean": "3.21.0", + "@lerna/cli": "3.18.5", + "@lerna/create": "3.22.0", + "@lerna/diff": "3.21.0", + "@lerna/exec": "3.21.0", + "@lerna/import": "3.22.0", + "@lerna/info": "3.21.0", + "@lerna/init": "3.21.0", + "@lerna/link": "3.21.0", + "@lerna/list": "3.21.0", + "@lerna/publish": "3.22.1", + "@lerna/run": "3.21.0", + "@lerna/version": "3.22.1", + "import-local": "^2.0.0", + "npmlog": "^4.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", + "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.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "lodash.xorby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.xorby/-/lodash.xorby-4.7.0.tgz", + "integrity": "sha1-nBmm+fBjputT3QPBtocXmYAUY9c=" + }, + "log4js": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", + "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", + "dev": true, + "requires": { + "date-format": "^3.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.1", + "rfdc": "^1.1.4", + "streamroller": "^2.2.4" + } + }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "dev": true, + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "loglevel": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", + "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==" + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "macos-release": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.9.tgz", + "integrity": "sha512-uHa4gv/NIdm9cUvfOhYb57nxrCY08iyMRXru0jbpaH57Q3NCge/ypY7fOvgCr8tPyucKrGbVndKhjXE0IX0VfQ==", + "requires": { + "agentkeepalive": "^4.1.0", + "cacache": "^15.0.0", + "http-cache-semantics": "^4.0.4", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^5.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "meow": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", + "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^2.5.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.13.1", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "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": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "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.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.1.tgz", + "integrity": "sha512-N0ddPAD8OZnoAHUYj1ZH4ZJVna+ucy7if777LrdeIV1ko8f46af4jbyM5EC1gN4xc9Wq5c3C38GnxRJ2gneXRA==", + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "mkdirp-promise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz", + "integrity": "sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE=", + "dev": true, + "requires": { + "mkdirp": "*" + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true + }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==", + "dev": true, + "optional": true + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimatch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", + "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", + "dev": true, + "requires": { + "array-differ": "^2.0.3", + "array-union": "^1.0.2", + "arrify": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "dev": true, + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "dev": true, + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true, + "optional": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nock": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.4.tgz", + "integrity": "sha512-alqTV8Qt7TUbc74x1pKRLSENzfjp4nywovcJgi/1aXDiUxXdt7TkruSTF5MDWPP7UoPVgea4F9ghVdmX0xxnSA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + } + }, + "node-fetch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", + "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==", + "dev": true + }, + "node-fetch-npm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz", + "integrity": "sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==", + "dev": true, + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-gyp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz", + "integrity": "sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", + "tar": "^4.4.12", + "which": "^1.3.1" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", + "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^6.3.0", + "shellwords": "^0.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-lifecycle": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz", + "integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==", + "dev": true, + "requires": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.15", + "node-gyp": "^5.0.2", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dev": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz", + "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-path": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz", + "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "octokit-pagination-methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-pipe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", + "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", + "dev": true + }, + "p-queue": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz", + "integrity": "sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg==", + "dev": true, + "requires": { + "eventemitter3": "^3.1.0" + } + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "p-waterfall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", + "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-path": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz", + "integrity": "sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0" + } + }, + "parse-url": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", + "integrity": "sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^3.3.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", + "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", + "dev": true + }, + "pretty-format": { + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz", + "integrity": "sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA==", + "requires": { + "@jest/types": "^26.3.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "dependencies": { + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + } + } + }, + "prompts": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", + "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, + "promzard": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "dev": true, + "requires": { + "read": "1" + } + }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + }, + "protoduck": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", + "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "dev": true, + "requires": { + "genfun": "^5.0.0" + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "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.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + } + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz", + "integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "dev": true, + "requires": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "read-package-tree": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", + "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", + "dev": true, + "requires": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "realpath-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", + "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", + "dev": true + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + } + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "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==" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "dev": true, + "optional": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.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" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socks": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.4.2.tgz", + "integrity": "sha512-cE6P9R+/CET9XhIMvqhRgaNT1GXKIahioMjwwhgYOd+8UQWP50rrFm+faqG/4xkZsjTdVeBYr0Tb246BSC9/fg==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz", + "integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==", + "requires": { + "agent-base": "6", + "debug": "4", + "socks": "^2.3.3" + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "^2.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "streamroller": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", + "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", + "dev": true, + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + } + } + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + } + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "dev": true + }, + "subscriptions-transport-ws": { + "version": "0.9.18", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz", + "integrity": "sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "teeny-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", + "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^3.3.2" + }, + "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "dev": true + }, + "temp-write": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", + "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "is-stream": "^1.1.0", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "requires": { + "tslib": "^1.9.3" + } + }, + "ts-jest": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.3.0.tgz", + "integrity": "sha512-Jq2uKfx6bPd9+JDpZNMBJMdMQUC3sJ08acISj8NXlVgR2d5OqslEHOR2KHMgwymu8h50+lKIm0m0xj/ioYdW2Q==", + "dev": true, + "requires": { + "@types/jest": "26.x", + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "26.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "18.x" + }, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "26.0.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.13.tgz", + "integrity": "sha512-sCzjKow4z9LILc6DhBvn5AkIfmQzDZkgtVVKmGwVrs5tuid38ws281D4l+7x1kP487+FlKDh5kfMZ8WSPAdmdA==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-util": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", + "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "dev": true, + "requires": { + "@jest/types": "^26.3.0", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", + "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "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": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, + "uglify-js": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", + "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true + }, + "umask": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", + "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universal-user-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "dev": true, + "requires": { + "os-name": "^3.1.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "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": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util-promisify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", + "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + }, + "v8-to-istanbul": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", + "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "dev": true, + "requires": { + "execa": "^1.0.0" + } + }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "dev": true, + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "dev": true, + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "write-json-file": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz", + "integrity": "sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==", + "dev": true, + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.15", + "make-dir": "^2.1.0", + "pify": "^4.0.1", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + } + }, + "write-pkg": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", + "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", + "dev": true, + "requires": { + "sort-keys": "^2.0.0", + "write-json-file": "^2.2.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "write-json-file": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", + "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", + "dev": true, + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.0.0" + } + } + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "xss": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", + "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "0.8.21", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", + "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..46b433b01 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "apollo-federation-monorepo", + "private": true, + "license": "MIT", + "repository": "github:apollographql/federation", + "scripts": { + "clean": "git clean -dfqX -- ./node_modules **/{dist,node_modules}/ ./*/tsconfig*tsbuildinfo", + "compile": "tsc --build tsconfig.build.json", + "compile:clean": "tsc --build tsconfig.build.json --clean", + "watch": "tsc --build tsconfig.build.json --watch", + "release:version-bump": "lerna version", + "release:start-ci-publish": "node -p '`Publish (dist-tag:${process.env.APOLLO_DIST_TAG || \"latest\"})`' | git tag -F - \"publish/$(date -u '+%Y%m%d%H%M%S')\" && git push origin \"$(git describe --match='publish/*' --tags --exact-match HEAD)\"", + "postinstall": "lerna run prepare && npm run compile", + "test": "jest --verbose", + "test:clean": "jest --clearCache", + "test:watch": "jest --verbose --watchAll", + "testonly": "npm test", + "test:ci": "npm run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", + "coverage": "npm test -- --coverage", + "coverage:upload": "codecov" + }, + "engines": { + "node": ">=6" + }, + "dependencies": { + "@apollographql/apollo-tools": "0.4.8", + "@apollo/federation": "file:federation-js", + "@apollo/gateway": "file:gateway-js", + "apollo-federation-integration-testsuite": "file:federation-integration-testsuite-js" + }, + "devDependencies": { + "@types/jest": "25.2.3", + "@types/lodash.xorby": "4.7.6", + "@types/loglevel": "1.5.4", + "@types/nock": "10.0.3", + "@types/node": "8.10.62", + "@types/node-fetch": "2.3.2", + "apollo-link": "1.2.14", + "apollo-link-http": "1.5.17", + "apollo-server": "2.17.0", + "apollo-server-core": "2.17.0", + "apollo-server-env": "2.4.5", + "apollo-server-testing": "2.17.0", + "apollo-server-types": "0.5.1", + "bunyan": "1.8.14", + "codecov": "3.7.2", + "deep-freeze": "0.0.1", + "graphql": "14.7.0", + "graphql-tag": "2.11.0", + "jest": "25.5.4", + "jest-config": "25.5.4", + "jest-cucumber": "2.0.13", + "jest-junit": "10.0.0", + "log4js": "6.3.0", + "lerna": "3.22.1", + "nock": "13.0.4", + "node-fetch": "2.3.0", + "prettier": "2.1.1", + "ts-jest": "26.3.0", + "typescript": "3.9.7", + "winston": "3.3.3", + "winston-transport": "4.4.0" + }, + "jest": { + "projects": [ + "/federation-js", + "/federation-integration-testsuite-js", + "/gateway-js" + ] + } +} diff --git a/query-planner-wasm/Cargo.toml b/query-planner-wasm/Cargo.toml index 7b9c60605..833d9f9ea 100644 --- a/query-planner-wasm/Cargo.toml +++ b/query-planner-wasm/Cargo.toml @@ -2,11 +2,11 @@ name = "query-planner-wasm" version = "0.0.2" # keep in sync with package.json authors = ["Apollo "] -homepage = "https://github.com/apollographql/rust" +homepage = "https://github.com/apollographql/federation" description = "Bridge code written in Rust to Javascript/Typescript, to be internally used by Apollo Gateway. This package is not meant to be independently consumed." edition = "2018" license = "MIT/Apache-2.0" -repository = "https://github.com/apollographql/rust" +repository = "https://github.com/apollographql/federation" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/query-planner-wasm/package.json b/query-planner-wasm/package.json index 372621070..dd3c6f02b 100644 --- a/query-planner-wasm/package.json +++ b/query-planner-wasm/package.json @@ -6,7 +6,8 @@ "scripts": {}, "repository": { "type": "git", - "url": "git+https://github.com/apollographql/rust.git" + "url": "git+https://github.com/apollographql/federation.git", + "directory": "query-planner-wasm/" }, "keywords": [ "GraphQL", @@ -17,7 +18,7 @@ "author": "opensource@apollographql.com", "license": "MIT", "bugs": { - "url": "https://github.com/apollographql/rust/issues" + "url": "https://github.com/apollographql/federation/issues" }, - "homepage": "https://github.com/apollographql/rust#readme" + "homepage": "https://github.com/apollographql/federation#readme" } diff --git a/query-planner/Cargo.toml b/query-planner/Cargo.toml index edabcb9b7..f0a77f62e 100644 --- a/query-planner/Cargo.toml +++ b/query-planner/Cargo.toml @@ -2,7 +2,7 @@ name = "apollo-query-planner" version = "0.0.1" authors = ["Apollo "] -homepage = "https://github.com/apollographql/rust" +homepage = "https://github.com/apollographql/federation" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..a54368e0b --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "composite": true, + "target": "es2016", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "removeComments": true, + "strict": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2017", "esnext.asynciterable"], + "types": ["node"], + "baseUrl": ".", + "paths": { + "*" : ["types/*"] + } + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 000000000..d75c8d1ee --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "include": [], + "references": [ + { "path": "./federation-js" }, + { "path": "./gateway-js" }, + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..a6997749c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.build.json" }, + { "path": "./tsconfig.test.json" }, + ] +} diff --git a/tsconfig.test.base.json b/tsconfig.test.base.json new file mode 100644 index 000000000..7d6d09611 --- /dev/null +++ b/tsconfig.test.base.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base", + "compilerOptions": { + "noEmit": true, + "lib": ["es2017", "es2019.array", "esnext.asynciterable"], + "types": ["node", "jest", "apollo-server-env/dist/global"], + "paths": { + "__mocks__/*" : ["__mocks__/*"], + } + } +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 000000000..688b867a9 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "include": [], + "references": [] +}