diff --git a/.circleci/config.yml b/.circleci/config.yml index e5a4f33d..b6d137b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,12 +82,23 @@ references: name: Start Neo4j Server command: ./scripts/start-neo4j.sh + restart_neo4j: &restart_neo4j + run: + name: Clear and restart Neo4j + command: ./scripts/stop-and-clear-neo4j.sh + start_graphql: &start_graphql run: name: Start GraphQL Server command: node -r @babel/register ./example/apollo-server/movies-middleware.js background: true + start_gateway: &start_gateway + run: + name: Start Apollo Gateway + command: npm run start-gateway + background: true + wait_for_graphql: &wait_for_graphql run: name: Wait for GraphQL server to be available @@ -103,6 +114,11 @@ references: name: Run Tests command: npm run test-all + run_gateway_tests: &run_gateway_tests + run: + name: Run Gateway tests + command: npm run test-gateway + env_neo4j34ee: &env_neo4j34ee NEO4J_DIST: 'enterprise' NEO4J_VERSION: '3.4.10' @@ -147,9 +163,12 @@ references: - *restore_neo4j - *start_neo4j - *start_graphql + - *start_gateway - *wait_for_graphql - *parse_tck - *run_tests + - *restart_neo4j + - *run_gateway_tests - *report_coverage jobs: diff --git a/example/apollo-federation/gateway.js b/example/apollo-federation/gateway.js new file mode 100644 index 00000000..6e4e0226 --- /dev/null +++ b/example/apollo-federation/gateway.js @@ -0,0 +1,115 @@ +import { ApolloServer } from 'apollo-server'; +import { ApolloGateway } from '@apollo/gateway'; +import { accountsSchema } from './services/accounts'; +import { inventorySchema } from './services/inventory'; +import { productsSchema } from './services/products'; +import { reviewsSchema } from './services/reviews'; +import neo4j from 'neo4j-driver'; + +// The schema and seed data are based on the Apollo Federation demo +// See: https://github.com/apollographql/federation-demo + +const driver = neo4j.driver( + process.env.NEO4J_URI || 'bolt://localhost:7687', + neo4j.auth.basic( + process.env.NEO4J_USER || 'neo4j', + process.env.NEO4J_PASSWORD || 'letmein' + ) +); + +// Start Accounts +const accountsService = new ApolloServer({ + schema: accountsSchema, + context: ({ req }) => { + return { + driver, + req, + cypherParams: { + userId: 'user-id' + } + }; + } +}); +accountsService.listen({ port: 4001 }).then(({ url }) => { + console.log(`🚀 Accounts ready at ${url}`); +}); + +// Start Reviews +const reviewsService = new ApolloServer({ + schema: reviewsSchema, + context: ({ req }) => { + return { + driver, + req, + cypherParams: { + userId: 'user-id' + } + }; + } +}); +reviewsService.listen({ port: 4002 }).then(({ url }) => { + console.log(`🚀 Reviews ready at ${url}`); +}); + +// Start Products +const productsService = new ApolloServer({ + schema: productsSchema, + context: ({ req }) => { + return { + driver, + req, + cypherParams: { + userId: 'user-id' + } + }; + } +}); +productsService.listen({ port: 4003 }).then(({ url }) => { + console.log(`🚀 Products ready at ${url}`); +}); + +// Start Inventory +const inventoryService = new ApolloServer({ + schema: inventorySchema, + context: ({ req }) => { + return { + driver, + req, + cypherParams: { + userId: 'user-id' + } + }; + } +}); +inventoryService.listen({ port: 4004 }).then(({ url }) => { + console.log(`🚀 Inventory ready at ${url}`); +}); + +const gateway = new ApolloGateway({ + serviceList: [ + { name: 'accounts', url: 'http://localhost:4001/graphql' }, + { name: 'reviews', url: 'http://localhost:4002/graphql' }, + { name: 'products', url: 'http://localhost:4003/graphql' }, + { name: 'inventory', url: 'http://localhost:4004/graphql' } + ], + // Experimental: Enabling this enables the query plan view in Playground. + __exposeQueryPlanExperimental: true +}); + +(async () => { + const server = new ApolloServer({ + gateway, + + // Apollo Graph Manager (previously known as Apollo Engine) + // When enabled and an `ENGINE_API_KEY` is set in the environment, + // provides metrics, schema management and trace reporting. + engine: false, + + // Subscriptions are unsupported but planned for a future Gateway version. + subscriptions: false + }); + + server.listen({ port: 4000 }).then(({ url }) => { + console.log(`🚀 Apollo Gateway ready at ${url}`); + }); +})(); diff --git a/example/apollo-federation/seed-data.js b/example/apollo-federation/seed-data.js new file mode 100644 index 00000000..363e68fe --- /dev/null +++ b/example/apollo-federation/seed-data.js @@ -0,0 +1,122 @@ +export const seedData = { + data: { + Review: [ + { + id: '1', + body: 'Love it!', + product: { + upc: '1', + name: 'Table', + price: 899, + weight: 100, + inStock: true, + metrics: [ + { + id: '100', + metric: 1, + data: 2 + } + ], + objectCompoundKey: { + id: '100', + metric: 1, + data: 2 + }, + listCompoundKey: [ + { + id: '100', + metric: 1, + data: 2 + } + ] + }, + authorID: '1', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2 + } + }, + { + id: '2', + body: 'Too expensive.', + product: { + upc: '2', + name: 'Couch', + price: 1299, + weight: 1000, + inStock: false, + metrics: [], + objectCompoundKey: null, + listCompoundKey: [] + }, + authorID: '1', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2 + } + }, + { + id: '3', + body: 'Could be better.', + product: { + upc: '3', + name: 'Chair', + price: 54, + weight: 50, + inStock: true, + metrics: [], + objectCompoundKey: null, + listCompoundKey: [] + }, + authorID: '2', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2 + } + }, + { + id: '4', + body: 'Prefer something else.', + product: { + upc: '1', + name: 'Table', + price: 899, + weight: 100, + inStock: true, + metrics: [ + { + id: '100', + metric: 1, + data: 2 + } + ], + objectCompoundKey: { + id: '100', + metric: 1, + data: 2 + }, + listCompoundKey: [ + { + id: '100', + metric: 1, + data: 2 + } + ] + }, + authorID: '2', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2 + } + } + ] + } +}; diff --git a/example/apollo-federation/services/accounts/index.js b/example/apollo-federation/services/accounts/index.js new file mode 100644 index 00000000..fecbb523 --- /dev/null +++ b/example/apollo-federation/services/accounts/index.js @@ -0,0 +1,67 @@ +import { gql } from 'apollo-server'; +import { buildFederatedSchema } from '@apollo/federation'; +import { neo4jgraphql, augmentTypeDefs, cypher } from '../../../../src'; + +// Example: without schema augmentation +export const accountsSchema = buildFederatedSchema([ + { + // Used to add support for neo4j-graphql directives + // (@cypher / @relation) and types (temporal / spatial) + typeDefs: augmentTypeDefs( + gql` + extend type Query { + me: Account @cypher(${cypher` + MATCH (account: Account { + id: '1' + }) + RETURN account + `}) + Account: [Account] @cypher(${cypher` + MATCH (account: Account) + RETURN account + `}) + } + + type Account @key(fields: "id") { + id: ID! + name: String + username: String + } + `, + { + isFederated: true + } + ), + resolvers: { + Query: { + async me(object, params, context, resolveInfo) { + return await neo4jgraphql(object, params, context, resolveInfo); + }, + async Account(object, params, context, resolveInfo) { + return await neo4jgraphql(object, params, context, resolveInfo); + } + }, + Account: { + // Base type reference resolver + async __resolveReference(object, context, resolveInfo) { + return await neo4jgraphql(object, {}, context, resolveInfo); + } + } + } + } +]); + +export const accounts = [ + { + id: '1', + name: 'Ada Lovelace', + birthDate: '1815-12-10', + username: '@ada' + }, + { + id: '2', + name: 'Alan Turing', + birthDate: '1912-06-23', + username: '@complete' + } +]; diff --git a/example/apollo-federation/services/inventory/index.js b/example/apollo-federation/services/inventory/index.js new file mode 100644 index 00000000..0f60a601 --- /dev/null +++ b/example/apollo-federation/services/inventory/index.js @@ -0,0 +1,84 @@ +import { gql } from 'apollo-server'; +import { buildFederatedSchema } from '@apollo/federation'; +import { makeAugmentedSchema, cypher } from '../../../../src'; + +export const inventorySchema = buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + extend type Product @key(fields: "upc listCompoundKey { id } objectCompoundKey { id } nullKey") { + upc: String! @external + weight: Int @external + price: Int @external + nullKey: String @external + inStock: Boolean + shippingEstimate: Int + @requires(fields: "weight price") + @cypher(${cypher` + CALL apoc.when($price > 900, + // free for expensive items + 'RETURN 0 AS value', + // estimate is based on weight + 'RETURN $weight * 0.5 AS value', + { + price: $price, + weight: $weight + }) + YIELD value + RETURN value.value + `}) + metrics: [Metric] + @requires(fields: "price") + @relation(name: "METRIC_OF", direction: OUT) + objectCompoundKey: Metric + @external + @relation(name: "METRIC_OF", direction: OUT) + listCompoundKey: [Metric] + @external + @relation(name: "METRIC_OF", direction: OUT) + } + + extend type Metric @key(fields: "id") { + id: ID @external + metric: Int @external + data: Int + @requires(fields: "metric") + @cypher(${cypher` + RETURN $metric + 1 + `}) + } + + `, + resolvers: { + Metric: { + // Generated + // async __resolveReference(object, context, resolveInfo) { + // const data = await neo4jgraphql(object, {}, context, resolveInfo); + // return { + // ...object, + // ...data + // }; + // }, + } + // Generated + // Product: { + // async __resolveReference(object, context, resolveInfo) { + // const data = await neo4jgraphql(object, {}, context, resolveInfo); + // return { + // ...object, + // ...data + // }; + // } + // } + }, + config: { + isFederated: true + // debug: true + } + }) +]); + +export const inventory = [ + { upc: '1', inStock: true }, + { upc: '2', inStock: false }, + { upc: '3', inStock: true } +]; diff --git a/example/apollo-federation/services/products/index.js b/example/apollo-federation/services/products/index.js new file mode 100644 index 00000000..6dc4462e --- /dev/null +++ b/example/apollo-federation/services/products/index.js @@ -0,0 +1,57 @@ +import { gql } from 'apollo-server'; +import { buildFederatedSchema } from '@apollo/federation'; +import { makeAugmentedSchema } from '../../../../src'; + +export const productsSchema = buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + extend type Query { + Product: [Product] + topProducts(first: Int = 5): [Product] + } + + type Product + @key( + fields: "upc listCompoundKey { id } objectCompoundKey { id } nullKey" + ) { + upc: String! + name: String + price: Int + weight: Int + nullKey: String + objectCompoundKey: Metric @relation(name: "METRIC_OF", direction: OUT) + listCompoundKey: [Metric] @relation(name: "METRIC_OF", direction: OUT) + } + + type Metric @key(fields: "id") { + id: ID + metric: Int + } + `, + config: { + isFederated: true + // debug: true + } + }) +]); + +export const products = [ + { + upc: '1', + name: 'Table', + price: 899, + weight: 100 + }, + { + upc: '2', + name: 'Couch', + price: 1299, + weight: 1000 + }, + { + upc: '3', + name: 'Chair', + price: 54, + weight: 50 + } +]; diff --git a/example/apollo-federation/services/reviews/index.js b/example/apollo-federation/services/reviews/index.js new file mode 100644 index 00000000..f9a0e115 --- /dev/null +++ b/example/apollo-federation/services/reviews/index.js @@ -0,0 +1,202 @@ +import { gql } from 'apollo-server'; +import { buildFederatedSchema } from '@apollo/federation'; +import { makeAugmentedSchema, neo4jgraphql, cypher } from '../../../../src'; +import { seedData } from '../../seed-data'; + +export const reviewsSchema = buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + type Review @key(fields: "id authorID") { + id: ID! + body: String + authorID: ID + # Scalar property to lookup associate node + author: Account + @cypher(${cypher` + MATCH (account:Account {id: this.authorID}) + RETURN account + `}) + # Normal use of @relation field directive + product: Product + @relation(name: "REVIEW_OF", direction: OUT) + } + + extend type Account @key(fields: "id") { + id: ID! @external + # Object list @relation field added to nonlocal type for local type + reviews(body: String): [Review] + @relation(name: "AUTHOR_OF", direction: OUT) + # Scalar @cypher field added to nonlocal type for local type + numberOfReviews: Int + @cypher(${cypher` + MATCH (this)-[:AUTHOR_OF]->(review:Review) + RETURN count(review) + `}) + } + + extend type Product @key(fields: "upc") { + upc: String! @external + reviews(body: String): [Review] + @relation(name: "REVIEW_OF", direction: IN) + } + + # Used in testing and for example of nested merge import + input MergeReviewsInput { + id: ID! + body: String + product: MergeProductInput + author: MergeAccountInput + } + + input MergeProductInput { + upc: String! + name: String + price: Int + weight: Int + inStock: Boolean + metrics: [MergeMetricInput] + objectCompoundKey: MergeMetricInput + listCompoundKey: [MergeMetricInput] + } + + input MergeMetricInput { + id: ID! + metric: String + } + + input MergeAccountInput { + id: ID! + name: String + username: String + } + + extend type Mutation { + MergeSeedData(data: MergeReviewsInput): Boolean @cypher(${cypher` + UNWIND $data AS review + MERGE (r:Review { + id: review.id + }) + SET r += { + body: review.body, + authorID: review.authorID + } + WITH * + + // Merge Review.author + UNWIND review.author AS account + MERGE (a:Account { + id: account.id + }) + ON CREATE SET a += { + name: account.name, + username: account.username + } + MERGE (r)<-[:AUTHOR_OF]-(a) + // Resets processing context for unwound sibling relationship data + WITH COUNT(*) AS SCOPE + + // Unwind second sibling, Review.product + UNWIND $data AS review + MATCH (r:Review { + id: review.id + }) + // Merge Review.product + UNWIND review.product AS product + MERGE (p:Product { + upc: product.upc + }) + ON CREATE SET p += { + name: product.name, + price: product.price, + weight: product.weight, + inStock: product.inStock + } + MERGE (p)<-[:REVIEW_OF]-(r) + WITH * + + // Merge Review.product.metrics / .objectCompoundKey / .listCompoundKey + UNWIND product.metrics AS metric + MERGE (m:Metric { + id: metric.id + }) + ON CREATE SET m += { + metric: metric.metric + } + MERGE (p)-[:METRIC_OF]->(m) + // End processing scope for Review.product + WITH COUNT(*) AS SCOPE + + RETURN true + `}) + DeleteSeedData: Boolean @cypher(${cypher` + MATCH (account: Account) + MATCH (product: Product) + MATCH (review: Review) + MATCH (metric: Metric) + DETACH DELETE account, product, review, metric + RETURN TRUE + `}) + } + + `, + resolvers: { + Mutation: { + async MergeSeedData(object, params, context, resolveInfo) { + const data = seedData.data['Review']; + return await neo4jgraphql(object, { data }, context, resolveInfo); + } + }, + Account: { + // Generated + // async __resolveReference(object, context, resolveInfo) { + // const data = await neo4jgraphql(object, {}, context, resolveInfo); + // return { + // ...object, + // ...data + // }; + // } + }, + Product: { + // Generated + // async __resolveReference(object, context, resolveInfo) { + // const data = await neo4jgraphql(object, {}, context, resolveInfo); + // return { + // ...object, + // ...data + // }; + // } + } + }, + config: { + isFederated: true + // debug: true + } + }) +]); + +export const reviews = [ + { + id: '1', + authorID: '1', + product: { upc: '1' }, + body: 'Love it!' + }, + { + id: '2', + authorID: '1', + product: { upc: '2' }, + body: 'Too expensive.' + }, + { + id: '3', + authorID: '2', + product: { upc: '3' }, + body: 'Could be better.' + }, + { + id: '4', + authorID: '2', + product: { upc: '1' }, + body: 'Prefer something else.' + } +]; diff --git a/package-lock.json b/package-lock.json index b4ee3d30..11bd4711 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,179 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollo/federation": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.14.1.tgz", + "integrity": "sha512-QuX2O3xO6mcTZdqttxHaMKWgq1v0nYRiDLe4k7DwAxVtb9nF8lsJDlup4Zicx3LBYhBCGQvumrYILuF/Amn6WQ==", + "dev": true, + "requires": { + "apollo-graphql": "^0.4.0", + "apollo-server-env": "^2.4.3", + "core-js": "^3.4.0", + "lodash.xorby": "^4.7.0" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "@apollo/gateway": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@apollo/gateway/-/gateway-0.14.1.tgz", + "integrity": "sha512-xFdXrGQ4ISmEFRQ1trrji1xTgH4Vbdh/dty7rdr486/RkXT4UmoNS9GVPoaqW+yO6MJUxohHYmu4CnppXllnfQ==", + "dev": true, + "requires": { + "@apollo/federation": "^0.14.1", + "@types/node-fetch": "2.5.4", + "apollo-engine-reporting-protobuf": "^0.4.4", + "apollo-env": "^0.6.1", + "apollo-graphql": "^0.4.0", + "apollo-server-caching": "^0.5.1", + "apollo-server-core": "^2.12.0", + "apollo-server-env": "^2.4.3", + "apollo-server-types": "^0.3.1", + "graphql-extensions": "^0.11.1", + "loglevel": "^1.6.1", + "make-fetch-happen": "^7.1.1", + "pretty-format": "^24.7.0" + }, + "dependencies": { + "@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==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "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 + }, + "apollo-cache-control": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.9.1.tgz", + "integrity": "sha512-9t2EcRevUrANuGhF5XUbKJEfnc6Jy2Rn7Y8nOIKlsEEC+AX7Ko4svWYTyyTxj0h0RXfiegY2nbz4sVry/pS3rA==", + "dev": true, + "requires": { + "apollo-server-env": "^2.4.3", + "graphql-extensions": "^0.11.1" + } + }, + "apollo-engine-reporting": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.7.1.tgz", + "integrity": "sha512-9ykddPxlC95R9CkkJaPaGriRbOGfzeKqqPXRAunyX1h4sG/8g+MJ/gGzmnNf63k6RvRUdRENCE83wPk2OeU+2A==", + "dev": true, + "requires": { + "apollo-engine-reporting-protobuf": "^0.4.4", + "apollo-graphql": "^0.4.0", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3", + "apollo-server-errors": "^2.4.1", + "apollo-server-types": "^0.3.1", + "async-retry": "^1.2.1", + "graphql-extensions": "^0.11.1" + } + }, + "apollo-server-core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.12.0.tgz", + "integrity": "sha512-BRVdOyZrRJ1ALlmis0vaOLIHHYu5K3UVKAQKIgHkRh/YY0Av4lpeEXr49ELK04LTeh0DG0pQ5YYYhaX1wFcDEw==", + "dev": true, + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "@apollographql/graphql-playground-html": "1.6.24", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^6.0.0", + "apollo-cache-control": "^0.9.1", + "apollo-datasource": "^0.7.0", + "apollo-engine-reporting": "^1.7.1", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3", + "apollo-server-errors": "^2.4.1", + "apollo-server-plugin-base": "^0.7.1", + "apollo-server-types": "^0.3.1", + "apollo-tracing": "^0.9.1", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.11.1", + "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-errors": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.1.tgz", + "integrity": "sha512-7oEd6pUxqyWYUbQ9TA8tM0NU/3aGtXSEibo6+txUkuHe7QaxfZ2wHRp+pfT1LC1K3RXYjKj61/C2xEO19s3Kdg==", + "dev": true + }, + "apollo-server-plugin-base": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.7.1.tgz", + "integrity": "sha512-PRavvoWq7/Xufqc+qkDQg3Aqueq4QrPBFfoCFIjhkJ4n2d2YoqE3gTGccb8YoWusfa62ASMn6R47OdNuVtEbXw==", + "dev": true, + "requires": { + "apollo-server-types": "^0.3.1" + } + }, + "apollo-server-types": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.3.1.tgz", + "integrity": "sha512-6nX5VC3icOGf1RZIs7/SYQZff+Cl16LQu1FHUOIk9gAMN2XjlRCyJgCeMj5YHJzQ8Mhg4BO0weWuydEg+JxLzg==", + "dev": true, + "requires": { + "apollo-engine-reporting-protobuf": "^0.4.4", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3" + } + }, + "apollo-tracing": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.9.1.tgz", + "integrity": "sha512-4wVNM6rc70XhwWxuDWrMBLaHA8NjB9pUS2sNpddQvP36ZtQfsa08XLSUxGAZT+bej+TzW26hKNtuO31RgqC9Hg==", + "dev": true, + "requires": { + "apollo-server-env": "^2.4.3", + "graphql-extensions": "^0.11.1" + } + }, + "graphql-extensions": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.11.1.tgz", + "integrity": "sha512-1bstq6YKaC579PTw9gchw2VlXqjPo3vn8NjRMaUqF2SxyYTjVSgXaCAbaeNa0B7xlLVigxi3DV1zh4A+ss+Lwg==", + "dev": true, + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "apollo-server-env": "^2.4.3", + "apollo-server-types": "^0.3.1" + } + }, + "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" + } + } + } + }, "@apollo/protobufjs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.3.tgz", @@ -1041,6 +1214,17 @@ } } }, + "@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" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -1311,6 +1495,31 @@ "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==", "dev": true }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@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==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, "@types/keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", @@ -1399,6 +1608,21 @@ "@types/node": "*" } }, + "@types/yargs": { + "version": "13.0.8", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz", + "integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==", + "dev": true, + "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==", + "dev": true + }, "@types/zen-observable": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", @@ -1439,6 +1663,33 @@ "negotiator": "0.6.2" } }, + "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 + }, + "agentkeepalive": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.2.tgz", + "integrity": "sha512-waNHE7tQBBn+2qXucI8HY0o2Y0OBPWldWOWsZwY71JcCm4SvrPnWdceFfB5NIXSqE8Ewq6VR/Qt5b1i69P6KCQ==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -1834,6 +2085,12 @@ "default-require-extensions": "^2.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -2573,6 +2830,50 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, + "cacache": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-14.0.0.tgz", + "integrity": "sha512-+Nr/BnA/tjAUXza9gH8F+FSP+1HvWqCKt4c95dQr4EDVJVafbzmPZpLKCkLYexs6vSd2B/1TOXrAoNnqVPfvRA==", + "dev": true, + "requires": { + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^7.0.0", + "tar": "^6.0.0", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + } + } + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -2784,6 +3085,12 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "chunkd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-1.0.0.tgz", @@ -3154,6 +3461,20 @@ "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" + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -3608,6 +3929,16 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3623,6 +3954,12 @@ "integrity": "sha1-IcoRLUirJLTh5//A5TOdMf38J0w=", "dev": true }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3668,6 +4005,21 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "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", @@ -4050,6 +4402,12 @@ "reusify": "^1.0.4" } }, + "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": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -4229,12 +4587,33 @@ "integrity": "sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==", "dev": true }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", "dev": true }, + "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", @@ -5161,6 +5540,35 @@ } } }, + "http-proxy-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-3.0.0.tgz", + "integrity": "sha512-uGuJaBWQWDQCJI5ip0d/VTYZW0nRrlLWXA4A7P1jrsa+f77rW2yXz315oBt6zGCF6l8C2tlMxY7ffULCj+5FhA==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + } + }, + "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" + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, "husky": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", @@ -5204,6 +5612,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, "ignore": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", @@ -5307,6 +5721,12 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "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==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5337,6 +5757,12 @@ "loose-envify": "^1.0.0" } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5516,6 +5942,12 @@ } } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, "is-npm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz", @@ -6336,6 +6768,12 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "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=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -6398,6 +6836,12 @@ } } }, + "loglevel": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", + "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", + "dev": true + }, "lolex": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", @@ -6454,6 +6898,29 @@ "semver": "^5.6.0" } }, + "make-fetch-happen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-7.1.1.tgz", + "integrity": "sha512-7fNjiOXNZhNGQzG5P15nU97aZQtzPU2GVgVd7pnqnl5gnpLzMAD8bAe5YG4iW2s0PTqaZy9xGv4Wfqe872kRNQ==", + "dev": true, + "requires": { + "agentkeepalive": "^4.1.0", + "cacache": "^14.0.0", + "http-cache-semantics": "^4.0.3", + "http-proxy-agent": "^3.0.0", + "https-proxy-agent": "^4.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.1.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^7.0.1" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6664,6 +7131,90 @@ } } }, + "minipass": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", + "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", + "dev": true, + "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==", + "dev": true + } + } + }, + "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==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.2.1.tgz", + "integrity": "sha512-ssHt0dkljEDaKmTgQ04DQgx2ag6G2gMPxA5hpcsoeTbfDgRf2fC2gNSRc6kISjD7ckCpHwwQvXxuTBK8402fXg==", + "dev": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-pipeline": "^1.2.2", + "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==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", + "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "dev": true, + "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==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "dev": true, + "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==", + "dev": true + } + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -6702,6 +7253,20 @@ } } }, + "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" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7746,6 +8311,30 @@ "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=", + "dev": true + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "dev": true, + "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=", + "dev": true + } + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -7846,6 +8435,12 @@ "strip-json-comments": "~2.0.1" } }, + "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==", + "dev": true + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -8219,6 +8814,15 @@ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", "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.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", @@ -8456,6 +9060,12 @@ } } }, + "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==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -8578,6 +9188,37 @@ } } }, + "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" + } + } + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -8682,6 +9323,16 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "ssri": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", + "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "minipass": "^3.1.1" + } + }, "stack-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", @@ -8942,6 +9593,34 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true }, + "tar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.1.tgz", + "integrity": "sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q==", + "dev": true, + "requires": { + "chownr": "^1.1.3", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -9195,6 +9874,24 @@ "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==", + "dev": true, + "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==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, "unique-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", diff --git a/package.json b/package.json index f098b56b..030fc78b 100755 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start-middleware": "nodemon ./example/apollo-server/movies-middleware.js --exec babel-node -e js", "start-typedefs": "nodemon ./example/apollo-server/movies-typedefs.js --exec babel-node -e js", "start-interface": "DEBUG=neo4j-graphql.js nodemon ./example/apollo-server/interface-union-example.js --exec babel-node -e js", + "start-gateway": "nodemon ./example/apollo-federation/gateway.js --exec babel-node -e js", "build": "babel src --presets @babel/preset-env --out-dir dist", "build-with-sourcemaps": "babel src --presets @babel/preset-env --out-dir dist --source-maps", "precommit": "lint-staged", @@ -19,9 +20,11 @@ "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "test-all": "nyc ava --verbose", "test-isolated": "nyc ava test/**/*.test.js --verbose --match='!*not-isolated*'", + "test-gateway": "nyc --reporter=lcov ava --fail-fast test/integration/gateway.test.js", "debug": "nodemon ./example/apollo-server/movies.js --exec babel-node --inspect-brk=9229 --nolazy", "debug-typedefs": "nodemon ./example/apollo-server/movies-typedefs.js --exec babel-node --inspect-brk=9229 --nolazy", - "debug-interface": "nodemon ./example/apollo-server/interfaceError.js --exec babel-node --inspect-brk=9229 --nolazy" + "debug-interface": "nodemon ./example/apollo-server/interfaceError.js --exec babel-node --inspect-brk=9229 --nolazy", + "debug-gateway": "nodemon ./example/apollo-federation/gateway.js --exec babel-node --inspect-brk=9229 --nolazy" }, "engines": { "node": ">=8" @@ -33,6 +36,8 @@ "url": "git+https://github.com/neo4j-graphql/neo4j-graphql-js" }, "devDependencies": { + "@apollo/federation": "^0.14.1", + "@apollo/gateway": "^0.14.1", "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", "@babel/node": "^7.0.0", @@ -72,7 +77,8 @@ "@babel/register" ], "files": [ - "!test/helpers" + "!test/helpers", + "!test/integration/gateway.test.js" ] }, "prettier": { diff --git a/scripts/stop-and-clear-neo4j.sh b/scripts/stop-and-clear-neo4j.sh new file mode 100755 index 00000000..3e151af4 --- /dev/null +++ b/scripts/stop-and-clear-neo4j.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +BOLT_PORT=7687 + +./neo4j/bin/neo4j stop +rm -r neo4j/data/databases/graph.db +./neo4j/bin/neo4j start + +echo "Waiting up to 2 minutes for neo4j bolt port ($BOLT_PORT)" + +for i in {1..120}; + do + nc -z 127.0.0.1 $BOLT_PORT + is_up=$? + if [ $is_up -eq 0 ]; then + echo + echo "Successfully started, neo4j bolt available on $BOLT_PORT" + break + fi + sleep 1 + echo -n "." +done +echo +# Wait a further 5 seconds after the port is available +sleep 5 \ No newline at end of file diff --git a/src/augment/ast.js b/src/augment/ast.js index f35ac5d6..0ec87752 100644 --- a/src/augment/ast.js +++ b/src/augment/ast.js @@ -4,7 +4,7 @@ import { TypeWrappers } from './fields'; /** * Builds the AST definition for a Name */ -export const buildName = ({ name = '' }) => ({ +export const buildName = ({ name = {} }) => ({ kind: Kind.NAME, value: name }); @@ -20,16 +20,16 @@ export const buildDocument = ({ definitions = [] }) => ({ /** * Builds the AST definition for a Directive Argument */ -export const buildDirectiveArgument = ({ name = '', value }) => ({ - kind: Kind.ARGUMENT, - name, - value -}); +export const buildDirectiveArgument = ({ name = {}, value }) => + buildArgument({ + name, + value + }); /** * Builds the AST definition for a Directive instance */ -export const buildDirective = ({ name = '', args = [] }) => ({ +export const buildDirective = ({ name = {}, args = [] }) => ({ kind: Kind.DIRECTIVE, name, arguments: args @@ -38,7 +38,7 @@ export const buildDirective = ({ name = '', args = [] }) => ({ /** * Builds the AST definition for a type */ -export const buildNamedType = ({ name = '', wrappers = {} }) => { +export const buildNamedType = ({ name = {}, wrappers = {} }) => { let type = { kind: Kind.NAMED_TYPE, name: buildName({ name }) @@ -86,7 +86,7 @@ export const buildOperationType = ({ operation = '', type = {} }) => ({ * Builds the AST definition for an Object type */ export const buildObjectType = ({ - name = '', + name = {}, fields = [], directives = [], description @@ -102,7 +102,7 @@ export const buildObjectType = ({ * Builds the AST definition for a Field */ export const buildField = ({ - name = '', + name = {}, type = {}, args = [], directives = [], @@ -121,7 +121,7 @@ export const buildField = ({ * used for both field arguments and input object types */ export const buildInputValue = ({ - name = '', + name = {}, type = {}, directives = [], defaultValue, @@ -140,7 +140,7 @@ export const buildInputValue = ({ /** * Builds the AST definition for an Enum type */ -export const buildEnumType = ({ name = '', values = [], description }) => ({ +export const buildEnumType = ({ name = {}, values = [], description }) => ({ kind: Kind.ENUM_TYPE_DEFINITION, name, values, @@ -150,7 +150,7 @@ export const buildEnumType = ({ name = '', values = [], description }) => ({ /** * Builds the AST definition for an Enum type value */ -export const buildEnumValue = ({ name = '', description }) => ({ +export const buildEnumValue = ({ name = {}, description }) => ({ kind: Kind.ENUM_VALUE_DEFINITION, name, description @@ -160,7 +160,7 @@ export const buildEnumValue = ({ name = '', description }) => ({ * Builds the AST definition for an Input Object type */ export const buildInputObjectType = ({ - name = '', + name = {}, fields = [], directives = [], description @@ -176,16 +176,84 @@ export const buildInputObjectType = ({ * Builds the AST definition for a Directive definition */ export const buildDirectiveDefinition = ({ - name = '', + name = {}, args = [], locations = [], - description + description, + isRepeatable = false }) => { return { kind: Kind.DIRECTIVE_DEFINITION, name, arguments: args, locations, - description + description, + isRepeatable + }; +}; + +export const buildDescription = ({ value, block = false }) => ({ + kind: Kind.STRING, + value, + block +}); + +export const buildSelectionSet = ({ selections = [] }) => { + return { + kind: Kind.SELECTION_SET, + selections + }; +}; + +export const buildFieldSelection = ({ + args = [], + directives = [], + name = {}, + selectionSet = {} +}) => { + return { + kind: Kind.FIELD, + arguments: args, + directives, + name, + selectionSet + }; +}; + +export const buildArgument = ({ name = {}, value }) => { + return { + kind: Kind.ARGUMENT, + name, + value + }; +}; + +export const buildVariableDefinition = ({ variable = {}, type = {} }) => { + return { + kind: Kind.VARIABLE_DEFINITION, + variable, + type + }; +}; + +export const buildVariable = ({ name = {} }) => { + return { + kind: Kind.VARIABLE, + name + }; +}; + +export const buildOperationDefinition = ({ + operation = '', + name = {}, + selectionSet = {}, + variableDefinitions = [] +}) => { + return { + kind: Kind.OPERATION_DEFINITION, + name, + operation, + selectionSet, + variableDefinitions }; }; diff --git a/src/augment/augment.js b/src/augment/augment.js index b7efea1f..7671cd20 100644 --- a/src/augment/augment.js +++ b/src/augment/augment.js @@ -12,12 +12,12 @@ import { augmentSchemaType, augmentTypes, transformNeo4jTypes, - regenerateSchemaType + regenerateSchemaType, + isSchemaDocument } from './types/types'; import { augmentDirectiveDefinitions } from './directives'; import { extractResolversFromSchema, augmentResolvers } from './resolvers'; import { addAuthDirectiveImplementations } from '../auth'; - /** * The main export for augmenting an SDL document */ @@ -34,7 +34,15 @@ export const makeAugmentedExecutableSchema = ({ config }) => { config = setDefaultConfig({ config }); - const definitions = parse(typeDefs).definitions; + const isParsedTypeDefs = isSchemaDocument({ definition: typeDefs }); + let definitions = []; + if (isParsedTypeDefs) { + // Print if we recieved parsed type definitions in a GraphQL Document + definitions = typeDefs.definitions; + } else { + // Otherwise parse the SDL and get its definitions + definitions = parse(typeDefs).definitions; + } let generatedTypeMap = {}; let [ typeDefinitionMap, @@ -81,12 +89,19 @@ export const makeAugmentedExecutableSchema = ({ const documentAST = buildDocument({ definitions: transformedDefinitions }); - const augmentedResolvers = augmentResolvers( + const augmentedResolvers = augmentResolvers({ generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, resolvers, config - ); + }); + if (config.isFederated === true) { + return { + typeDefs: documentAST, + resolvers: augmentedResolvers + }; + } resolverValidationOptions.requireResolversForResolveType = false; return makeExecutableSchema({ typeDefs: print(documentAST), @@ -155,12 +170,19 @@ export const augmentedSchema = (schema, config) => { definitions: transformedDefinitions }); const resolvers = extractResolversFromSchema(schema); - const augmentedResolvers = augmentResolvers( + const augmentedResolvers = augmentResolvers({ generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, resolvers, config - ); + }); + if (config.isFederated === true) { + return { + typeDefs: documentAST, + resolvers: augmentedResolvers + }; + } return makeExecutableSchema({ typeDefs: print(documentAST), resolvers: augmentedResolvers, @@ -198,6 +220,7 @@ export const mapDefinitions = ({ definitions = [], config = {} }) => { }); const [typeMap, operationTypeMap] = initializeOperationTypes({ typeDefinitionMap, + typeExtensionDefinitionMap, schemaTypeDefinition, config }); @@ -287,7 +310,7 @@ const APIConfiguration = { /** * Builds the default values in a given configuration object */ -const setDefaultConfig = ({ config = {} }) => { +export const setDefaultConfig = ({ config = {} }) => { const configKeys = Object.keys(config); Object.values(APIConfiguration).forEach(configKey => { if (!configKeys.find(providedKey => providedKey === configKey)) { diff --git a/src/augment/fields.js b/src/augment/fields.js index e5b8cc9e..6e35551a 100644 --- a/src/augment/fields.js +++ b/src/augment/fields.js @@ -147,6 +147,18 @@ export const unwrapNamedType = ({ type = {}, unwrappedType = {} }) => { export const getFieldDefinition = ({ fields = [], name = '' }) => fields.find(field => field.name && field.name.value === name); +/** + * A getter for a field definition of a given name, contained + * in the field definitions of a type in a given array of extensions + */ +export const getTypeExtensionFieldDefinition = ({ + typeExtensions = [], + name = '' +}) => + typeExtensions.find(extension => + getFieldDefinition({ fields: extension.fields, name }) + ); + /** * A getter for the type name of a field of a given name, * finding the field, unwrapping its type, and returning diff --git a/src/augment/resolvers.js b/src/augment/resolvers.js index 0152b04a..70772f63 100644 --- a/src/augment/resolvers.js +++ b/src/augment/resolvers.js @@ -1,48 +1,91 @@ import { neo4jgraphql } from '../index'; import { OperationType } from '../augment/types/types'; +import { + generateBaseTypeReferenceResolvers, + generateNonLocalTypeExtensionReferenceResolvers +} from '../federation'; /** * The main export for the generation of resolvers for the * Query and Mutation API. Prevent overwriting. */ -export const augmentResolvers = ( - augmentedTypeMap, +export const augmentResolvers = ({ + generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, resolvers, - config -) => { + config = {} +}) => { + const isFederated = config.isFederated; // Persist and generate Query resolvers let queryTypeName = OperationType.QUERY; + let mutationTypeName = OperationType.MUTATION; + let subscriptionTypeName = OperationType.SUBSCRIPTION; + const queryType = operationTypeMap[queryTypeName]; - if (queryType) { - queryTypeName = queryType.name.value; + if (queryType) queryTypeName = queryType.name.value; + const queryTypeExtensions = typeExtensionDefinitionMap[queryTypeName]; + if (queryType || (queryTypeExtensions && queryTypeExtensions.length)) { let queryResolvers = resolvers && resolvers[queryTypeName] ? resolvers[queryTypeName] : {}; - queryResolvers = possiblyAddResolvers(queryType, queryResolvers, config); - if (Object.keys(queryResolvers).length > 0) { + queryResolvers = possiblyAddResolvers({ + operationType: queryType, + operationTypeExtensions: queryTypeExtensions, + resolvers: queryResolvers, + config, + isFederated + }); + + if (Object.keys(queryResolvers).length) { resolvers[queryTypeName] = queryResolvers; + if (isFederated) { + resolvers = generateBaseTypeReferenceResolvers({ + queryResolvers, + resolvers, + config + }); + } } } + + if (Object.values(typeExtensionDefinitionMap).length) { + if (isFederated) { + resolvers = generateNonLocalTypeExtensionReferenceResolvers({ + resolvers, + generatedTypeMap, + typeExtensionDefinitionMap, + queryTypeName, + mutationTypeName, + subscriptionTypeName, + config + }); + } + } + // Persist and generate Mutation resolvers - let mutationTypeName = OperationType.MUTATION; const mutationType = operationTypeMap[mutationTypeName]; - if (mutationType) { - mutationTypeName = mutationType.name.value; + if (mutationType) mutationTypeName = mutationType.name.value; + const mutationTypeExtensions = typeExtensionDefinitionMap[mutationTypeName]; + if ( + mutationType || + (mutationTypeExtensions && mutationTypeExtensions.length) + ) { let mutationResolvers = resolvers && resolvers[mutationTypeName] ? resolvers[mutationTypeName] : {}; - mutationResolvers = possiblyAddResolvers( - mutationType, - mutationResolvers, + mutationResolvers = possiblyAddResolvers({ + operationType: mutationType, + operationTypeExtensions: mutationTypeExtensions, + resolvers: mutationResolvers, config - ); + }); if (Object.keys(mutationResolvers).length > 0) { resolvers[mutationTypeName] = mutationResolvers; } } + // Persist Subscription resolvers - let subscriptionTypeName = OperationType.SUBSCRIPTION; const subscriptionType = operationTypeMap[subscriptionTypeName]; if (subscriptionType) { subscriptionTypeName = subscriptionType.name.value; @@ -54,13 +97,14 @@ export const augmentResolvers = ( resolvers[subscriptionTypeName] = subscriptionResolvers; } } + // must implement __resolveInfo for every Interface type // we use "FRAGMENT_TYPE" key to identify the Interface implementation // type at runtime, so grab this value - const derivedTypes = Object.keys(augmentedTypeMap).filter( + const derivedTypes = Object.keys(generatedTypeMap).filter( e => - augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' || - augmentedTypeMap[e].kind === 'UnionTypeDefinition' + generatedTypeMap[e].kind === 'InterfaceTypeDefinition' || + generatedTypeMap[e].kind === 'UnionTypeDefinition' ); derivedTypes.map(e => { resolvers[e] = {}; @@ -69,31 +113,47 @@ export const augmentResolvers = ( return obj['FRAGMENT_TYPE']; }; }); + return resolvers; }; +const getOperationFieldMap = ({ operationType, operationTypeExtensions }) => { + const fieldMap = {}; + const fields = operationType ? operationType.fields : []; + fields.forEach(field => { + fieldMap[field.name.value] = true; + }); + operationTypeExtensions.forEach(extension => { + extension.fields.forEach(field => { + fieldMap[field.name.value] = true; + }); + }); + return fieldMap; +}; + /** * Generates resolvers for a given operation type, if * any fields exist, for any resolver not provided */ -const possiblyAddResolvers = (operationType, resolvers, config) => { - let operationName = ''; - const fields = operationType ? operationType.fields : []; - const operationTypeMap = fields.reduce((acc, t) => { - acc[t.name.value] = t; - return acc; - }, {}); - return Object.keys(operationTypeMap).reduce((acc, t) => { - // if no resolver provided for this operation type field - operationName = operationTypeMap[t].name.value; +const possiblyAddResolvers = ({ + operationType, + operationTypeExtensions = [], + resolvers, + config +}) => { + const fieldMap = getOperationFieldMap({ + operationType, + operationTypeExtensions + }); + Object.keys(fieldMap).forEach(name => { // If not provided - if (acc[operationName] === undefined) { - acc[operationName] = function(...args) { - return neo4jgraphql(...args, config.debug); + if (resolvers[name] === undefined) { + resolvers[name] = async function(...args) { + return await neo4jgraphql(...args, config.debug); }; } - return acc; - }, resolvers); + }); + return resolvers; }; /** diff --git a/src/augment/types/node/mutation.js b/src/augment/types/node/mutation.js index 0c804990..2cc57c7c 100644 --- a/src/augment/types/node/mutation.js +++ b/src/augment/types/node/mutation.js @@ -14,7 +14,12 @@ import { import { getPrimaryKey } from '../../../utils'; import { shouldAugmentType } from '../../augment'; import { OperationType } from '../../types/types'; -import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields'; +import { + TypeWrappers, + getFieldDefinition, + isNeo4jIDField, + getTypeExtensionFieldDefinition +} from '../../fields'; /** * An enum describing the names of node type mutations @@ -38,6 +43,7 @@ export const augmentNodeMutationAPI = ({ propertyInputValues, generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, config }) => { const primaryKey = getPrimaryKey(definition); @@ -57,6 +63,7 @@ export const augmentNodeMutationAPI = ({ typeName, propertyInputValues, operationTypeMap, + typeExtensionDefinitionMap, config }); }); @@ -76,14 +83,21 @@ const buildNodeMutationField = ({ typeName, propertyInputValues, operationTypeMap, + typeExtensionDefinitionMap, config }) => { const mutationFields = mutationType.fields; const mutationName = `${mutationAction}${typeName}`; + const mutationTypeName = mutationType ? mutationType.name.value : ''; + const mutationTypeExtensions = typeExtensionDefinitionMap[mutationTypeName]; if ( !getFieldDefinition({ fields: mutationFields, name: mutationName + }) && + !getTypeExtensionFieldDefinition({ + typeExtensions: mutationTypeExtensions, + name: typeName }) ) { const mutationField = { diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js index 66a2ff28..aa4d9e17 100644 --- a/src/augment/types/node/node.js +++ b/src/augment/types/node/node.js @@ -1,3 +1,4 @@ +import { Kind } from 'graphql'; import { augmentNodeQueryAPI, augmentNodeQueryArgumentTypes, @@ -55,10 +56,50 @@ export const augmentNodeType = ({ typeDefinitionMap, generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, config }) => { + let nodeInputTypeMap = {}; + let propertyOutputFields = []; + let propertyInputValues = []; + let extensionPropertyInputValues = []; + let extensionNodeInputTypeMap = {}; if (isObjectType || isInterfaceType || isUnionType) { - let [ + const typeExtensions = typeExtensionDefinitionMap[typeName] || []; + if (typeExtensions.length) { + typeExtensionDefinitionMap[typeName] = typeExtensions.map(extension => { + let isIgnoredType = false; + const isObjectExtension = extension.kind === Kind.OBJECT_TYPE_EXTENSION; + const isInterfaceExtension = + extension.kind === Kind.INTERFACE_TYPE_EXTENSION; + if (isObjectExtension || isInterfaceExtension) { + [ + extensionNodeInputTypeMap, + propertyOutputFields, + extensionPropertyInputValues, + isIgnoredType + ] = augmentNodeTypeFields({ + typeName, + definition: extension, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + nodeInputTypeMap: extensionNodeInputTypeMap, + propertyInputValues: extensionPropertyInputValues, + propertyOutputFields, + config + }); + if (!isIgnoredType) { + extension.fields = propertyOutputFields; + } + } + return extension; + }); + } + + // A type is ignored when all its fields use @neo4j_ignore + let isIgnoredType = false; + [ nodeInputTypeMap, propertyOutputFields, propertyInputValues, @@ -71,9 +112,19 @@ export const augmentNodeType = ({ typeDefinitionMap, generatedTypeMap, operationTypeMap, + nodeInputTypeMap, + extensionNodeInputTypeMap, + propertyOutputFields, + propertyInputValues, config }); - // A type is ignored when all its fields use @neo4j_ignore + + definition.fields = propertyOutputFields; + + if (extensionPropertyInputValues.length) { + propertyInputValues.push(...extensionPropertyInputValues); + } + if (!isIgnoredType) { if (!isOperationType && !isInterfaceType && !isUnionType) { [propertyOutputFields, nodeInputTypeMap] = buildNeo4jSystemIDField({ @@ -86,7 +137,6 @@ export const augmentNodeType = ({ }); } [ - propertyOutputFields, typeDefinitionMap, generatedTypeMap, operationTypeMap @@ -102,14 +152,19 @@ export const augmentNodeType = ({ propertyInputValues, nodeInputTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap, config }); - definition.fields = propertyOutputFields; } } - return [definition, generatedTypeMap, operationTypeMap]; + return [ + definition, + generatedTypeMap, + operationTypeMap, + typeExtensionDefinitionMap + ]; }; /** @@ -125,23 +180,35 @@ export const augmentNodeTypeFields = ({ typeDefinitionMap, generatedTypeMap, operationTypeMap, + nodeInputTypeMap = {}, + extensionNodeInputTypeMap, + propertyOutputFields = [], + propertyInputValues = [], + isUnionExtension, + isObjectExtension, + isInterfaceExtension, config }) => { - let nodeInputTypeMap = {}; - let propertyOutputFields = []; - const propertyInputValues = []; let isIgnoredType = true; - if (!isUnionType) { + if (!isUnionType && !isUnionExtension) { const fields = definition.fields; if (!isQueryType) { - nodeInputTypeMap[FilteringArgument.FILTER] = { - name: `_${typeName}Filter`, - fields: [] - }; - nodeInputTypeMap[OrderingArgument.ORDER_BY] = { - name: `_${typeName}Ordering`, - values: [] - }; + if (!nodeInputTypeMap[FilteringArgument.FILTER]) { + nodeInputTypeMap[FilteringArgument.FILTER] = { + name: `_${typeName}Filter`, + fields: [] + }; + } + if (!nodeInputTypeMap[OrderingArgument.ORDER_BY]) { + nodeInputTypeMap[OrderingArgument.ORDER_BY] = { + name: `_${typeName}Ordering`, + values: [] + }; + } + } + if (fields === undefined) { + console.log('\ndefinition: ', definition); + console.log('fields: ', fields); } propertyOutputFields = fields.reduce((outputFields, field) => { let fieldType = field.type; @@ -160,6 +227,8 @@ export const augmentNodeTypeFields = ({ name: DirectiveDefinition.RELATION }); if ( + !isObjectExtension && + !isInterfaceExtension && isPropertyTypeField({ kind: outputKind, type: outputType @@ -199,7 +268,9 @@ export const augmentNodeTypeFields = ({ operationTypeMap, config, relationshipDirective, - outputTypeWrappers + outputTypeWrappers, + isObjectExtension, + isInterfaceExtension }); } else if (isRelationshipType({ definition: outputDefinition })) { [ @@ -234,6 +305,23 @@ export const augmentNodeTypeFields = ({ }); return outputFields; }, []); + + if (!isQueryType && extensionNodeInputTypeMap) { + if (extensionNodeInputTypeMap[FilteringArgument.FILTER]) { + const extendedFilteringFields = + extensionNodeInputTypeMap[FilteringArgument.FILTER].fields; + nodeInputTypeMap[FilteringArgument.FILTER].fields.push( + ...extendedFilteringFields + ); + } + if (extensionNodeInputTypeMap[OrderingArgument.ORDER_BY]) { + const extendedOrderingValues = + extensionNodeInputTypeMap[OrderingArgument.ORDER_BY].values; + nodeInputTypeMap[OrderingArgument.ORDER_BY].values.push( + ...extendedOrderingValues + ); + } + } } else { isIgnoredType = false; } @@ -263,7 +351,9 @@ const augmentNodeTypeField = ({ operationTypeMap, config, relationshipDirective, - outputTypeWrappers + outputTypeWrappers, + isObjectExtension, + isInterfaceExtension }) => { const isUnionType = isUnionTypeDefinition({ definition: outputDefinition }); fieldArguments = augmentNodeTypeFieldArguments({ @@ -275,7 +365,7 @@ const augmentNodeTypeField = ({ typeDefinitionMap, config }); - if (!isUnionType) { + if (!isUnionType && !isObjectExtension && !isInterfaceExtension) { if ( relationshipDirective && !isQueryTypeDefinition({ definition, operationTypeMap }) @@ -337,10 +427,10 @@ const augmentNodeTypeAPI = ({ isUnionType, isOperationType, isQueryType, - propertyOutputFields, propertyInputValues, nodeInputTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap, config @@ -353,6 +443,7 @@ const augmentNodeTypeAPI = ({ propertyInputValues, generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, config }); generatedTypeMap = buildNodeSelectionInputType({ @@ -373,16 +464,12 @@ const augmentNodeTypeAPI = ({ propertyInputValues, nodeInputTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap, config }); - return [ - propertyOutputFields, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap - ]; + return [typeDefinitionMap, generatedTypeMap, operationTypeMap]; }; /** diff --git a/src/augment/types/node/query.js b/src/augment/types/node/query.js index 67b7e3de..92093016 100644 --- a/src/augment/types/node/query.js +++ b/src/augment/types/node/query.js @@ -16,6 +16,7 @@ import { OperationType } from '../../types/types'; import { TypeWrappers, getFieldDefinition, + getTypeExtensionFieldDefinition, Neo4jSystemIDField } from '../../fields'; import { @@ -48,6 +49,7 @@ export const augmentNodeQueryAPI = ({ propertyInputValues, nodeInputTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap, config @@ -63,6 +65,7 @@ export const augmentNodeQueryAPI = ({ propertyInputValues, operationTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, config }); } @@ -151,13 +154,20 @@ const buildNodeQueryField = ({ propertyInputValues, operationTypeMap, typeDefinitionMap, + typeExtensionDefinitionMap, config }) => { const queryFields = queryType.fields; + const queryTypeName = queryType ? queryType.name.value : ''; + const queryTypeExtensions = typeExtensionDefinitionMap[queryTypeName]; if ( !getFieldDefinition({ fields: queryFields, name: typeName + }) && + !getTypeExtensionFieldDefinition({ + typeExtensions: queryTypeExtensions, + name: typeName }) ) { queryFields.push( diff --git a/src/augment/types/types.js b/src/augment/types/types.js index 6924445e..dbbe4fb1 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -5,7 +5,8 @@ import { GraphQLString, GraphQLInt, GraphQLFloat, - GraphQLBoolean + GraphQLBoolean, + isTypeExtensionNode } from 'graphql'; import { isIgnoredField, @@ -95,6 +96,12 @@ export const Neo4jDataType = { } }; +/** + * A predicate function for identifying a Document AST resulting + * from the parsing of SDL type definitions + */ +export const isSchemaDocument = ({ definition = {} }) => + typeof definition === 'object' && definition.kind === Kind.DOCUMENT; /** * A predicate function for identifying type definitions representing * a Neo4j node entity @@ -147,10 +154,11 @@ export const isOperationTypeDefinition = ({ /** * A predicate function for identifying the GraphQL Query type definition */ -export const isQueryTypeDefinition = ({ definition, operationTypeMap }) => - definition.name && operationTypeMap[OperationType.QUERY] +export const isQueryTypeDefinition = ({ definition, operationTypeMap }) => { + return definition.name && operationTypeMap[OperationType.QUERY] ? definition.name.value === operationTypeMap[OperationType.QUERY].name.value : false; +}; /** * A predicate function for identifying the GraphQL Mutation type definition @@ -173,6 +181,11 @@ export const isSubscriptionTypeDefinition = ({ operationTypeMap[OperationType.SUBSCRIPTION].name.value : false; +const isDefaultOperationType = ({ typeName }) => + typeName === OperationType.QUERY || + typeName === OperationType.MUTATION || + typeName === OperationType.SUBSCRIPTION; + /** * A predicate function for identifying a GraphQL type definition representing * complex Neo4j property types (Temporal, Spatial) managed by the translation process @@ -240,15 +253,18 @@ export const interpretType = ({ definition = {} }) => { */ export const augmentTypes = ({ typeDefinitionMap, - typeExtensionDefinitionMap, + typeExtensionDefinitionMap = {}, generatedTypeMap, operationTypeMap = {}, config = {} }) => { - Object.entries({ - ...typeDefinitionMap, - ...operationTypeMap - }).forEach(([typeName, definition]) => { + const augmentationDefinitions = [ + ...Object.entries({ + ...typeDefinitionMap, + ...operationTypeMap + }) + ]; + augmentationDefinitions.forEach(([typeName, definition]) => { const isObjectType = isObjectTypeDefinition({ definition }); const isInterfaceType = isInterfaceTypeDefinition({ definition }); const isUnionType = isUnionTypeDefinition({ definition }); @@ -258,10 +274,10 @@ export const augmentTypes = ({ }); const isQueryType = isQueryTypeDefinition({ definition, operationTypeMap }); if (isOperationType) { - // Overwrite existing operation map entry with augmented type - operationTypeMap[typeName] = augmentOperationType({ + [definition, typeExtensionDefinitionMap] = augmentOperationType({ typeName, definition, + typeExtensionDefinitionMap, isQueryType, isObjectType, typeDefinitionMap, @@ -269,8 +285,14 @@ export const augmentTypes = ({ operationTypeMap, config }); + operationTypeMap[typeName] = definition; } else if (isNodeType({ definition })) { - [definition, generatedTypeMap, operationTypeMap] = augmentNodeType({ + [ + definition, + generatedTypeMap, + operationTypeMap, + typeExtensionDefinitionMap + ] = augmentNodeType({ typeName, definition, isObjectType, @@ -281,6 +303,7 @@ export const augmentTypes = ({ typeDefinitionMap, generatedTypeMap, operationTypeMap, + typeExtensionDefinitionMap, config }); // Add augmented type to generated type map @@ -289,12 +312,57 @@ export const augmentTypes = ({ // Persist any other type definition generatedTypeMap[typeName] = definition; } - return definition; }); generatedTypeMap = augmentNeo4jTypes({ generatedTypeMap, config }); + Object.entries(typeExtensionDefinitionMap).forEach( + ([typeName, extensions]) => { + const isNonLocalType = !generatedTypeMap[typeName]; + const isOperationType = isDefaultOperationType({ typeName }); + if (isNonLocalType && !isOperationType) { + const augmentedExtensions = extensions.map(definition => { + const isObjectExtension = + definition.kind === Kind.OBJECT_TYPE_EXTENSION; + const isInterfaceExtension = + definition.kind === Kind.INTERFACE_TYPE_EXTENSION; + const isUnionExtension = + definition.kind === Kind.UNION_TYPE_EXTENSION; + let nodeInputTypeMap = {}; + let propertyOutputFields = []; + let propertyInputValues = []; + let extensionNodeInputTypeMap = {}; + if (isObjectExtension || isInterfaceExtension) { + [ + nodeInputTypeMap, + propertyOutputFields, + propertyInputValues + ] = augmentNodeTypeFields({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + nodeInputTypeMap, + extensionNodeInputTypeMap, + propertyOutputFields, + propertyInputValues, + isUnionExtension, + isObjectExtension, + isInterfaceExtension, + config + }); + return { + ...definition, + fields: propertyOutputFields + }; + } + }); + typeExtensionDefinitionMap[typeName] = augmentedExtensions; + } + } + ); return [typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap]; }; @@ -451,6 +519,7 @@ export const transformNeo4jTypes = ({ definitions = [], config }) => { export const initializeOperationTypes = ({ typeDefinitionMap, schemaTypeDefinition, + typeExtensionDefinitionMap, config = {} }) => { let queryTypeName = OperationType.QUERY; @@ -489,7 +558,8 @@ export const initializeOperationTypes = ({ queryTypeName, mutationTypeName, subscriptionTypeName, - typeDefinitionMap + typeDefinitionMap, + typeExtensionDefinitionMap }); return [typeDefinitionMap, operationTypeMap]; }; @@ -574,6 +644,7 @@ const buildAugmentationTypeMaps = ({ const augmentOperationType = ({ typeName, definition, + typeExtensionDefinitionMap, isQueryType, isObjectType, typeDefinitionMap, @@ -582,7 +653,40 @@ const augmentOperationType = ({ config }) => { if (isQueryType && isObjectType) { - let [ + let nodeInputTypeMap = {}; + let propertyOutputFields = []; + let propertyInputValues = []; + const typeExtensions = typeExtensionDefinitionMap[typeName] || []; + if (typeExtensions.length) { + typeExtensionDefinitionMap[typeName] = typeExtensions.map(extension => { + let isIgnoredType = false; + [ + nodeInputTypeMap, + propertyOutputFields, + propertyInputValues, + isIgnoredType + ] = augmentNodeTypeFields({ + typeName, + definition: extension, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + nodeInputTypeMap, + propertyOutputFields, + propertyInputValues, + config + }); + // FIXME fieldArguments are modified through reference so + // this branch doesn't end up mattereing. A case of isIgnoredType + // being true may also be highly improbable, though it is posisble + if (!isIgnoredType) { + extension.fields = propertyOutputFields; + } + return extension; + }); + } + let isIgnoredType = false; + [ nodeInputTypeMap, propertyOutputFields, propertyInputValues, @@ -592,6 +696,7 @@ const augmentOperationType = ({ definition, typeDefinitionMap, generatedTypeMap, + propertyOutputFields, operationTypeMap, config }); @@ -599,7 +704,7 @@ const augmentOperationType = ({ definition.fields = propertyOutputFields; } } - return definition; + return [definition, typeExtensionDefinitionMap]; }; /** diff --git a/src/federation.js b/src/federation.js new file mode 100644 index 00000000..9a8a7673 --- /dev/null +++ b/src/federation.js @@ -0,0 +1,491 @@ +import { parse, isScalarType, GraphQLList, isEnumType } from 'graphql'; +import { ApolloError } from 'apollo-server'; +import { + buildSelectionSet, + buildFieldSelection, + buildName, + buildVariableDefinition, + buildVariable, + buildArgument, + buildOperationDefinition +} from './augment/ast'; +import { checkRequestError } from './auth'; +import { neo4jgraphql } from './index'; + +export const NEO4j_GRAPHQL_SERVICE = 'Neo4jGraphQLService'; + +const CONTEXT_KEYS_PATH = `__${NEO4j_GRAPHQL_SERVICE}`; + +const SERVICE_VARIABLE = '_SERVICE_'; + +const SERVICE_FIELDS = { + ENTITIES: '_entities' +}; + +const SERVICE_FIELD_ARGUMENTS = { + REPRESENTATIONS: 'representations' +}; + +const INTROSPECTION_FIELD = { + TYPENAME: '__typename' +}; + +const REFERENCE_RESOLVER_NAME = '__resolveReference'; + +export const executeFederatedOperation = async ({ + object, + params, + context, + resolveInfo, + debugFlag +}) => { + const requestError = checkRequestError(context); + if (requestError) throw new Error(requestError); + const [typeName, parentTypeData] = parseRepresentation({ + object, + resolveInfo + }); + const schema = resolveInfo.schema; + const entityType = schema.getType(typeName); + const operationResolveInfo = buildResolveInfo({ + parentTypeData, + typeName, + entityType, + resolveInfo, + schema + }); + const operationContext = setOperationContext({ + typeName, + parentTypeData, + params, + context, + resolveInfo + }); + const data = await neo4jgraphql( + {}, + params, + operationContext, + operationResolveInfo, + debugFlag + ); + return decideOperationPayload({ data }); +}; + +export const isFederatedOperation = ({ resolveInfo = {} }) => + resolveInfo.fieldName === SERVICE_FIELDS.ENTITIES; + +export const setCompoundKeyFilter = ({ params = {}, compoundKeys = {} }) => { + if (Object.keys(compoundKeys).length) { + const filterArgument = Object.entries(compoundKeys).reduce( + (filterArgument, [fieldName, value]) => { + // compound key for a list field of an object type uses AND filter + if (Array.isArray(value)) { + filterArgument[fieldName] = { + AND: value + }; + } else { + filterArgument[fieldName] = value; + } + return filterArgument; + }, + {} + ); + params['filter'] = filterArgument; + } + return params; +}; + +export const getFederatedOperationData = ({ context }) => { + const [entityKeys, requiredData, params] = context[CONTEXT_KEYS_PATH] || {}; + const compoundKeys = {}; + const scalarKeys = {}; + Object.entries(entityKeys).forEach(([serviceParam, value]) => { + if (typeof value === 'object') { + compoundKeys[serviceParam] = value; + } else { + scalarKeys[serviceParam] = value; + } + }); + return { + scalarKeys, + compoundKeys, + requiredData, + params + }; +}; + +const setOperationContext = ({ + typeName, + context = {}, + parentTypeData = {}, + params = {}, + resolveInfo +}) => { + const entityType = resolveInfo.schema.getType(typeName); + const extensionASTNodes = entityType.extensionASTNodes; + const keyFieldMap = buildTypeExtensionKeyFieldMap({ + entityType, + extensionASTNodes + }); + const requiredFieldMap = getTypeExtensionRequiredFieldMap({ + parentTypeData, + keyFieldMap + }); + const [keyData, requiredData] = Object.entries(parentTypeData).reduce( + ([keyData, requiredData], [name, value]) => { + if (keyFieldMap[name]) { + keyData[name] = value; + } else if (requiredFieldMap[name]) { + requiredData[name] = value; + } + return [keyData, requiredData]; + }, + [{}, {}] + ); + context[CONTEXT_KEYS_PATH] = [keyData, requiredData, params]; + return context; +}; + +const parseRepresentation = ({ object = {}, resolveInfo }) => { + let { [INTROSPECTION_FIELD.TYPENAME]: typeName, ...fieldData } = object; + if (!typeName) { + // Set default + typeName = getEntityTypeName({ + resolveInfo + }); + } + // Error if still no typeName + if (typeName === undefined) { + throw new ApolloError('Missing __typename key'); + } + // Prepare provided key and required field data + // for translation, removing nulls + const parentTypeData = getDefinedKeys({ + fieldData + }); + return [typeName, parentTypeData]; +}; + +const getEntityTypeName = ({ resolveInfo = {} }) => { + const operation = resolveInfo.operation || {}; + const rootSelection = operation.selectionSet + ? operation.selectionSet.selections[0] + : {}; + const entityFragment = rootSelection.selectionSet + ? rootSelection.selectionSet.selections[0] + : {}; + const typeCondition = entityFragment + ? entityFragment.typeCondition.name.value + : undefined; + return typeCondition; +}; + +const getDefinedKeys = ({ fieldData = {}, parentTypeData = {} }) => { + Object.entries(fieldData).forEach(([key, value]) => { + const isList = Array.isArray(value); + const isNotEmptyList = !isList || value.length; + if ( + key !== INTROSPECTION_FIELD.TYPENAME && + value !== null && + isNotEmptyList + ) { + // When no value is returned for a field in a compound key + // it's value is null and should be removed to prevent a + // _not filter translation + if (!isList && typeof value === 'object') { + const definedKeys = getDefinedKeys({ + fieldData: value + }); + // Keep it if at least one key has a valid value + if (definedKeys && Object.values(definedKeys).length) { + parentTypeData[key] = definedKeys; + } + } else { + parentTypeData[key] = value; + } + } + }); + return parentTypeData; +}; + +const buildTypeExtensionKeyFieldMap = ({ + extensionASTNodes = [], + entityType = {} +}) => { + const entityTypeAst = entityType.astNode; + const entityTypeDirectives = entityTypeAst.directives || []; + let keyFieldMap = getFederationDirectiveFields({ + directives: entityTypeDirectives, + directiveName: 'key' + }); + extensionASTNodes.map(type => { + const directives = type.directives; + keyFieldMap = getFederationDirectiveFields({ + directives, + keyFieldMap, + directiveName: 'key' + }); + }); + return keyFieldMap; +}; + +const getTypeExtensionRequiredFieldMap = ({ + parentTypeData = {}, + keyFieldMap = {} +}) => { + // Infers that any entity field value which is not a key + // is provided given the use of a @requires directive + let requiredFieldMap = {}; + Object.keys(parentTypeData).forEach(fieldName => { + if (keyFieldMap[fieldName] === undefined) { + requiredFieldMap[fieldName] = true; + } + }); + return requiredFieldMap; +}; + +const getFederationDirectiveFields = ({ + directives = [], + keyFieldMap = {}, + directiveName = '' +}) => { + directives.forEach(directive => { + const name = directive.name.value; + if (name === directiveName) { + const fields = directive.arguments.find( + arg => arg.name.value === 'fields' + ); + if (fields) { + const fieldsArgument = fields.value.value; + const parsedKeyFields = parse(`{ ${fieldsArgument} }`); + const definitions = parsedKeyFields.definitions; + const selections = definitions[0].selectionSet.selections; + selections.forEach(field => { + const name = field.name.value; + keyFieldMap[name] = true; + }); + } + } + }); + return keyFieldMap; +}; + +const buildArguments = ({ entityType, parentTypeData, resolveInfo }) => { + const entityFields = entityType.getFields(); + const { + [SERVICE_FIELD_ARGUMENTS.REPRESENTATIONS]: representations, + ...variableValues + } = resolveInfo.variableValues; + const operation = resolveInfo.operation; + const variableDefinitions = operation.variableDefinitions.filter( + ({ variable }) => { + return variable.name.value !== SERVICE_FIELD_ARGUMENTS.REPRESENTATIONS; + } + ); + const [ + keyFieldArguments, + keyVariableDefinitions, + keyVariableValues + ] = Object.values(entityFields).reduce( + ([keyFieldArguments, keyVariableDefinitions, keyVariableValues], field) => { + const astNode = field.astNode; + let name = astNode.name.value; + const type = astNode.type; + if (isScalarType(field.type) || isEnumType(field.type)) { + const hasKeyFieldArgument = parentTypeData[name] !== undefined; + if (hasKeyFieldArgument) { + const serviceVariableName = `${SERVICE_VARIABLE}${name}`; + keyVariableValues[serviceVariableName] = parentTypeData[name]; + keyFieldArguments.push( + buildArgument({ + name: buildName({ + name + }), + value: buildName({ + name: `$${serviceVariableName}` + }) + }) + ); + // keyVariableDefinitions are not currently used but could be + // so they're built here for now and we scope the variable name + keyVariableDefinitions.push( + buildVariableDefinition({ + variable: buildVariable({ + name: buildName({ + name: serviceVariableName + }) + }), + type + }) + ); + } + } + return [keyFieldArguments, keyVariableDefinitions, keyVariableValues]; + }, + [[], [], {}] + ); + const mergedVariableValues = { + ...keyVariableValues, + ...variableValues + }; + variableDefinitions.unshift(...keyVariableDefinitions); + return [keyFieldArguments, variableDefinitions, mergedVariableValues]; +}; + +const getSelectionSet = ({ typeName, keyFieldArguments, resolveInfo }) => { + let selectionSet = {}; + if (resolveInfo.fieldNodes) { + selectionSet = resolveInfo.fieldNodes[0].selectionSet; + // Get the selections inside the fragment provided on the entity type + selectionSet = selectionSet.selections[0].selectionSet; + selectionSet = buildSelectionSet({ + selections: [ + buildFieldSelection({ + name: buildName({ + name: typeName + }), + args: keyFieldArguments, + selectionSet + }) + ] + }); + } + if (!Object.keys(selectionSet).length) { + throw new ApolloError( + `Failed to extract the expected selectionSet for the entity ${typeName}` + ); + } + return selectionSet; +}; + +const buildResolveInfo = ({ + parentTypeData, + typeName, + entityType, + resolveInfo, + schema +}) => { + const fieldName = typeName; + const path = { key: typeName }; + const [ + keyFieldArguments, + variableDefinitions, + variableValues + ] = buildArguments({ + entityType, + parentTypeData, + resolveInfo + }); + const selectionSet = getSelectionSet({ + typeName, + keyFieldArguments, + resolveInfo + }); + const fieldNodes = selectionSet.selections; + const operation = buildOperationDefinition({ + operation: 'query', + name: buildName({ + name: NEO4j_GRAPHQL_SERVICE + }), + selectionSet, + variableDefinitions + }); + // Assume a list query and extract in decideOperationPayload + const returnType = new GraphQLList(entityType); + return { + fieldName, + fieldNodes, + returnType, + path, + schema, + operation, + variableValues + // Unused by neo4jgraphql translation + // parentType: undefined, + // fragments: undefined, + // rootValue: undefined + }; +}; + +const decideOperationPayload = ({ data }) => { + const dataExists = data !== undefined; + const isListData = dataExists && Array.isArray(data); + if (dataExists && isListData && data.length) { + data = data[0]; + } + return data; +}; + +export const generateBaseTypeReferenceResolvers = ({ + queryResolvers = {}, + resolvers = {}, + config +}) => { + Object.keys(queryResolvers).forEach(typeName => { + // Initialize type resolver object + if (resolvers[typeName] === undefined) resolvers[typeName] = {}; + // If not provided + if (resolvers[typeName][REFERENCE_RESOLVER_NAME] === undefined) { + resolvers[typeName][REFERENCE_RESOLVER_NAME] = async function( + object, + context, + resolveInfo + ) { + return await neo4jgraphql( + object, + {}, + context, + resolveInfo, + config.debug + ); + }; + } + }); + return resolvers; +}; + +export const generateNonLocalTypeExtensionReferenceResolvers = ({ + resolvers, + generatedTypeMap, + typeExtensionDefinitionMap, + queryTypeName, + mutationTypeName, + subscriptionTypeName, + config +}) => { + Object.keys(typeExtensionDefinitionMap).forEach(typeName => { + if ( + typeName !== queryTypeName && + typeName !== mutationTypeName && + typeName !== subscriptionTypeName + ) { + if (generatedTypeMap[typeName] === undefined) { + // Initialize type resolver object + if (resolvers[typeName] === undefined) resolvers[typeName] = {}; + // If not provided + if (resolvers[typeName][REFERENCE_RESOLVER_NAME] === undefined) { + resolvers[typeName][REFERENCE_RESOLVER_NAME] = async function( + object, + context, + resolveInfo + ) { + const entityData = await neo4jgraphql( + object, + {}, + context, + resolveInfo, + config.debug + ); + return { + // Data for this entity type possibly previously fetched from other services + ...object, + // Data now fetched for the fields this service resolves for the entity type + ...entityData + }; + }; + } + } + } + }); + return resolvers; +}; diff --git a/src/index.js b/src/index.js index 2c493af0..207332aa 100644 --- a/src/index.js +++ b/src/index.js @@ -16,9 +16,14 @@ import { mapDefinitions, mergeDefinitionMaps } from './augment/augment'; -import { augmentTypes, transformNeo4jTypes } from './augment/types/types'; +import { + augmentTypes, + transformNeo4jTypes, + isSchemaDocument +} from './augment/types/types'; import { buildDocument } from './augment/ast'; import { augmentDirectiveDefinitions } from './augment/directives'; +import { isFederatedOperation, executeFederatedOperation } from './federation'; const neo4jGraphQLVersion = require('../package.json').version; @@ -31,78 +36,90 @@ export async function neo4jgraphql( resolveInfo, debugFlag ) { - // throw error if context.req.error exists - if (checkRequestError(context)) { - throw new Error(checkRequestError(context)); - } + if (isFederatedOperation({ resolveInfo })) { + return await executeFederatedOperation({ + object, + params, + context, + resolveInfo, + debugFlag + }); + } else { + // throw error if context.req.error exists + if (checkRequestError(context)) { + throw new Error(checkRequestError(context)); + } - if (!context.driver) { - throw new Error( - "No Neo4j JavaScript driver instance provided. Please ensure a Neo4j JavaScript driver instance is injected into the context object at the key 'driver'." - ); - } + if (!context.driver) { + throw new Error( + "No Neo4j JavaScript driver instance provided. Please ensure a Neo4j JavaScript driver instance is injected into the context object at the key 'driver'." + ); + } - let query; - let cypherParams; + let query; + let cypherParams; - const cypherFunction = isMutation(resolveInfo) ? cypherMutation : cypherQuery; - [query, cypherParams] = cypherFunction( - params, - context, - resolveInfo, - debugFlag - ); - - if (debugFlag) { - console.log(` -Deprecation Warning: Remove \`debug\` parameter and use an environment variable -instead: \`DEBUG=neo4j-graphql-js\`. - `); - console.log(query); - console.log(JSON.stringify(cypherParams, null, 2)); - } + const cypherFunction = isMutation(resolveInfo) + ? cypherMutation + : cypherQuery; + [query, cypherParams] = cypherFunction( + params, + context, + resolveInfo, + debugFlag + ); + + if (debugFlag) { + console.log(` + Deprecation Warning: Remove \`debug\` parameter and use an environment variable + instead: \`DEBUG=neo4j-graphql-js\`. + `); + console.log(query); + console.log(JSON.stringify(cypherParams, null, 2)); + } - debug('%s', query); - debug('%s', JSON.stringify(cypherParams, null, 2)); + debug('%s', query); + debug('%s', JSON.stringify(cypherParams, null, 2)); - context.driver._userAgent = `neo4j-graphql-js/${neo4jGraphQLVersion}`; + context.driver._userAgent = `neo4j-graphql-js/${neo4jGraphQLVersion}`; - let session; + let session; - if (context.neo4jDatabase) { - // database is specified in context object - try { - // connect to the specified database - // must be using 4.x version of driver - session = context.driver.session({ - database: context.neo4jDatabase - }); - } catch (e) { - // error - not using a 4.x version of driver! - // fall back to default database + if (context.neo4jDatabase) { + // database is specified in context object + try { + // connect to the specified database + // must be using 4.x version of driver + session = context.driver.session({ + database: context.neo4jDatabase + }); + } catch (e) { + // error - not using a 4.x version of driver! + // fall back to default database + session = context.driver.session(); + } + } else { + // no database specified session = context.driver.session(); } - } else { - // no database specified - session = context.driver.session(); - } - let result; + let result; - try { - if (isMutation(resolveInfo)) { - result = await session.writeTransaction(tx => { - return tx.run(query, cypherParams); - }); - } else { - result = await session.readTransaction(tx => { - return tx.run(query, cypherParams); - }); + try { + if (isMutation(resolveInfo)) { + result = await session.writeTransaction(tx => { + return tx.run(query, cypherParams); + }); + } else { + result = await session.readTransaction(tx => { + return tx.run(query, cypherParams); + }); + } + } finally { + session.close(); } - } finally { - session.close(); + return extractQueryResult(result, resolveInfo.returnType); } - return extractQueryResult(result, resolveInfo.returnType); } export function cypherQuery( @@ -152,7 +169,16 @@ export function cypherMutation( export const augmentTypeDefs = (typeDefs, config = {}) => { config.query = false; config.mutation = false; - const definitions = parse(typeDefs).definitions; + if (config.isFederated === undefined) config.isFederated = false; + const isParsedTypeDefs = isSchemaDocument({ definition: typeDefs }); + let definitions = []; + if (isParsedTypeDefs) { + // Print if we recieved parsed type definitions in a GraphQL Document + definitions = typeDefs.definitions; + } else { + // Otherwise parse the SDL and get its definitions + definitions = parse(typeDefs).definitions; + } let generatedTypeMap = {}; let [ typeDefinitionMap, @@ -194,8 +220,10 @@ export const augmentTypeDefs = (typeDefs, config = {}) => { const documentAST = buildDocument({ definitions: transformedDefinitions }); - typeDefs = print(documentAST); - return typeDefs; + if (config.isFederated === true) { + return documentAST; + } + return print(documentAST); }; export const augmentSchema = (schema, config) => { @@ -243,3 +271,19 @@ export const inferSchema = (driver, config = {}) => { return tree.initialize().then(graphQLMapper); }; + +export const cypher = (statement, ...substitutions) => { + // Get the array of string literals + const literals = statement.raw; + // Add each substitution inbetween all + const composed = substitutions.reduce((composed, substitution, index) => { + // Add the string literal + composed.push(literals[index]); + // Add the substution proceeding it + composed.push(substitution); + return composed; + }, []); + // Add the last literal + composed.push(literals[literals.length - 1]); + return `statement: """${composed.join('')}"""`; +}; diff --git a/src/selections.js b/src/selections.js index 7c773db9..dd2ef65b 100644 --- a/src/selections.js +++ b/src/selections.js @@ -49,7 +49,9 @@ export function buildCypherSelection({ resolveInfo, paramIndex = 1, parentSelectionInfo = {}, - secondParentSelectionInfo = {} + secondParentSelectionInfo = {}, + isFederatedOperation = false, + context }) { if (!selections.length) return [initial, {}]; const typeMap = resolveInfo.schema.getTypeMap(); @@ -132,7 +134,9 @@ export function buildCypherSelection({ resolveInfo, shallowFilterParams, parentSelectionInfo, - secondParentSelectionInfo + secondParentSelectionInfo, + isFederatedOperation, + context }; let translationConfig = undefined; @@ -270,7 +274,9 @@ export function buildCypherSelection({ paramIndex, cypherParams, parentSelectionInfo, - secondParentSelectionInfo + secondParentSelectionInfo, + isFederatedOperation, + context }); } else if (isObjectType || isInterfaceType) { const schemaTypeRelation = getRelationTypeDirective(schemaTypeAstNode); @@ -306,7 +312,9 @@ export function buildCypherSelection({ selections, paramIndex }, - secondParentSelectionInfo: parentSelectionInfo + secondParentSelectionInfo: parentSelectionInfo, + isFederatedOperation, + context }); const fieldArgs = @@ -374,7 +382,9 @@ export function buildCypherSelection({ subSelection, skipLimit, commaIfTail, - tailParams + tailParams, + isFederatedOperation, + context }); } else if (isNeo4jType(fieldTypeName)) { translationConfig = neo4jType({ @@ -500,7 +510,9 @@ const translateScalarTypeField = ({ paramIndex, cypherParams, parentSelectionInfo, - secondParentSelectionInfo + secondParentSelectionInfo, + isFederatedOperation, + context }) => { if (fieldName === Neo4jSystemIDField) { return { @@ -521,7 +533,9 @@ const translateScalarTypeField = ({ cypherParams, schemaType, resolveInfo, - paramIndex + paramIndex, + isFederatedOperation, + context )}}, false)${commaIfTail}`, ...tailParams }; @@ -741,18 +755,18 @@ const mergeInterfacedObjectFragments = ({ }; const mergeFragmentedSelections = ({ selections = [] }) => { - const subSelecionFieldMap = {}; + const subSelectionFieldMap = {}; const fragments = []; selections.forEach(selection => { const fieldKind = selection.kind; if (fieldKind === Kind.FIELD) { const fieldName = selection.name.value; - if (!subSelecionFieldMap[fieldName]) { + if (!subSelectionFieldMap[fieldName]) { // initialize entry for this composing type - subSelecionFieldMap[fieldName] = selection; + subSelectionFieldMap[fieldName] = selection; } else { - const alreadySelected = subSelecionFieldMap[fieldName].selectionSet - ? subSelecionFieldMap[fieldName].selectionSet.selections + const alreadySelected = subSelectionFieldMap[fieldName].selectionSet + ? subSelectionFieldMap[fieldName].selectionSet.selections : []; const selected = selection.selectionSet ? selection.selectionSet.selections @@ -760,7 +774,7 @@ const mergeFragmentedSelections = ({ selections = [] }) => { // If the field has a subselection (relationship field) if (alreadySelected.length && selected.length) { const selections = [...alreadySelected, ...selected]; - subSelecionFieldMap[ + subSelectionFieldMap[ fieldName ].selectionSet.selections = mergeFragmentedSelections({ selections @@ -769,11 +783,14 @@ const mergeFragmentedSelections = ({ selections = [] }) => { } } else { // Persist all fragments, to be merged later - fragments.push(selection); + // If we already have this fragment, skip it. + if (!fragments.some(anyElement => anyElement === selection)) { + fragments.push(selection); + } } }); // Return the aggregation of all fragments and merged relationship fields - return [...Object.values(subSelecionFieldMap), ...fragments]; + return [...Object.values(subSelectionFieldMap), ...fragments]; }; export const getDerivedTypes = ({ diff --git a/src/translate.js b/src/translate.js index a807ac24..f806d79a 100644 --- a/src/translate.js +++ b/src/translate.js @@ -33,9 +33,7 @@ import { neo4jTypePredicateClauses, isNeo4jType, isTemporalType, - isNeo4jTypeInput, - isTemporalInputType, - isSpatialInputType, + isSpatialType, isSpatialDistanceInputType, isGraphqlScalarType, isGraphqlInterfaceType, @@ -43,7 +41,6 @@ import { innerType, relationDirective, typeIdentifiers, - decideNeo4jTypeConstructor, getAdditionalLabels, getInterfaceDerivedTypeNames, getPayloadSelections, @@ -55,8 +52,7 @@ import { isEnumType, isObjectType, isInterfaceType, - isInputType, - isListType + Kind } from 'graphql'; import { buildCypherSelection, @@ -67,7 +63,16 @@ import { } from './selections'; import _ from 'lodash'; import neo4j from 'neo4j-driver'; -import { isUnionTypeDefinition } from './augment/types/types'; +import { + isUnionTypeDefinition, + isUnionTypeExtensionDefinition +} from './augment/types/types'; +import { + getFederatedOperationData, + setCompoundKeyFilter, + NEO4j_GRAPHQL_SERVICE +} from './federation'; +import { unwrapNamedType } from './augment/fields'; const derivedTypesParamName = schemaTypeName => `${schemaTypeName}_derivedTypes`; @@ -123,7 +128,9 @@ export const customCypherField = ({ subSelection, skipLimit, commaIfTail, - tailParams + tailParams, + isFederatedOperation, + context }) => { const [mapProjection, labelPredicate] = buildMapProjection({ isComputedField: true, @@ -157,7 +164,9 @@ export const customCypherField = ({ cypherParams, schemaType, resolveInfo, - cypherFieldParamsIndex + cypherFieldParamsIndex, + isFederatedOperation, + context )}}, true) ${labelPredicate}| ${ labelPredicate ? `${nestedVariable}] | ` : '' }${mapProjection}]${headListWrapperSuffix}${skipLimit} ${commaIfTail}`, @@ -324,9 +333,7 @@ export const relationTypeFieldOnNodeType = ({ cypherParams }) => { if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { - tailParams.initial = `${initial}${fieldName}: {${ - subSelection[0] - }}${skipLimit} ${commaIfTail}`; + tailParams.initial = `${initial}${fieldName}: {${subSelection[0]}}${skipLimit} ${commaIfTail}`; return [tailParams, subSelection]; } const relationshipVariableName = `${nestedVariable}_relation`; @@ -577,9 +584,7 @@ const directedNodeTypeFieldOnRelationType = ({ }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`; return [tailParams, subSelection]; } else { - tailParams.initial = `${initial}${fieldName}: ${variableName} {${ - subSelection[0] - }}${skipLimit} ${commaIfTail}`; + tailParams.initial = `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`; // Case of a renamed directed field // e.g., 'from: Movie' -> 'Movie: Movie' return [tailParams, subSelection]; @@ -734,9 +739,7 @@ export const neo4jType = ({ return { initial: `${initial}${fieldName}: ${ fieldIsArray - ? `reduce(a = [], INSTANCE IN ${variableName}.${fieldName} | a + {${ - subSelection[0] - }})${commaIfTail}` + ? `reduce(a = [], INSTANCE IN ${variableName}.${fieldName} | a + {${subSelection[0]}})${commaIfTail}` : temporalOrderingFieldExists(parentSchemaType, parentFilterParams) ? `${safeVariableName}.${fieldName}${commaIfTail}` : `{${subSelection[0]}}${commaIfTail}` @@ -763,16 +766,48 @@ export const translateQuery = ({ const isInterfaceType = isGraphqlInterfaceType(schemaType); const isUnionType = isGraphqlUnionType(schemaType); const isObjectType = isGraphqlObjectType(schemaType); - - const [nullParams, nonNullParams] = filterNullParams({ + let [nullParams, nonNullParams] = filterNullParams({ offset, first, otherParams }); - const filterParams = getFilterParams(nonNullParams); - const queryArgs = getQueryArguments(resolveInfo); + + // Check is this is a federated operation, in which case get the lookup keys + const operation = resolveInfo.operation || {}; + // check if the operation name is the name used for generated queries + const isFederatedOperation = + operation.name && operation.name.value === NEO4j_GRAPHQL_SERVICE; + const queryTypeCypherDirective = getQueryCypherDirective( + resolveInfo, + isFederatedOperation + ); + let scalarKeys = {}; + let compoundKeys = {}; + let requiredData = {}; + if (isFederatedOperation) { + const operationData = getFederatedOperationData({ context }); + scalarKeys = operationData.scalarKeys; + compoundKeys = operationData.compoundKeys; + requiredData = operationData.requiredData; + if (queryTypeCypherDirective) { + // all nonnull keys become available as cypher variables + nonNullParams = { + ...scalarKeys, + ...compoundKeys, + ...requiredData + }; + } else { + // all scalar keys get used as field arguments, while relationship + // field keys being translated as a filter argument + nonNullParams = { + ...scalarKeys + }; + } + } + + let filterParams = getFilterParams(nonNullParams); + const queryArgs = getQueryArguments(resolveInfo, isFederatedOperation); const neo4jTypeArgs = getNeo4jTypeArguments(queryArgs); - const queryTypeCypherDirective = getQueryCypherDirective(resolveInfo); const cypherParams = getCypherParams(context); const queryParams = paramsToString( innerFilterParams( @@ -784,7 +819,6 @@ export const translateQuery = ({ cypherParams ); const safeVariableName = safeVar(variableName); - const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, safeVariableName, @@ -831,8 +865,21 @@ export const translateQuery = ({ }); } else { const additionalLabels = getAdditionalLabels(schemaType, cypherParams); + if (isFederatedOperation) { + nonNullParams = setCompoundKeyFilter({ + params: nonNullParams, + compoundKeys + }); + nonNullParams = { + ...nonNullParams, + ...otherParams, + ...requiredData + }; + } return nodeQuery({ resolveInfo, + isFederatedOperation, + context, cypherParams, schemaType, argString: queryParams, @@ -1009,6 +1056,8 @@ const customQuery = ({ // Generated API const nodeQuery = ({ resolveInfo, + isFederatedOperation, + context, cypherParams, schemaType, selections, @@ -1039,11 +1088,14 @@ const nodeQuery = ({ variableName, schemaType, resolveInfo, - paramIndex: rootParamIndex + paramIndex: rootParamIndex, + isFederatedOperation, + context }); - const fieldArgs = getQueryArguments(resolveInfo); + const fieldArgs = getQueryArguments(resolveInfo, isFederatedOperation); const [filterPredicates, serializedFilter] = processFilterArgument({ fieldArgs, + isFederatedOperation, schemaType, variableName, resolveInfo, @@ -1182,8 +1234,9 @@ const getUnionLabels = ({ typeName = '', typeMap = {} }) => { const definition = typeMap[key]; const astNode = definition.astNode; if (isUnionTypeDefinition({ definition: astNode })) { - const unionTypeName = astNode.name.value; - if (astNode.types.find(type => type.name.value === typeName)) { + const types = definition.getTypes(); + const unionTypeName = definition.name; + if (types.find(type => type.name === typeName)) { unionLabels.push(unionTypeName); } } @@ -1216,6 +1269,7 @@ export const translateMutation = ({ [resolveInfo.fieldName].astNode.directives.find(x => { return x.name.value === 'MutationMeta'; }); + const params = initializeMutationParams({ mutationMeta, resolveInfo, @@ -1236,6 +1290,7 @@ export const translateMutation = ({ typeof schemaType.getInterfaces === 'function' ? schemaType.getInterfaces().map(i => i.name) : []; + const unionLabels = getUnionLabels({ typeName, typeMap }); const additionalLabels = [ ...additionalNodeLabels, @@ -2059,6 +2114,7 @@ const buildSortMultiArgs = param => { const processFilterArgument = ({ fieldArgs, + isFederatedOperation, schemaType, variableName, resolveInfo, @@ -2067,21 +2123,19 @@ const processFilterArgument = ({ rootIsRelationType = false }) => { const filterArg = fieldArgs.find(e => e.name.value === 'filter'); - const filterValue = Object.keys(params).length ? params['filter'] : undefined; const filterParamKey = paramIndex > 1 ? `${paramIndex - 1}_filter` : `filter`; const filterCypherParam = `$${filterParamKey}`; let translations = []; - // if field has both a filter argument and argument data is provided - if (filterArg && filterValue) { + // allows an exception for the existence of the filter argument AST + // if isFederatedOperation + if ((filterArg || isFederatedOperation) && filterValue) { + // if field has both a filter argument and argument data is provided const schema = resolveInfo.schema; - const typeName = getNamedType(filterArg).type.name.value; - const filterSchemaType = schema.getType(typeName); - // get fields of filter type - const typeFields = filterSchemaType.getFields(); - const [filterFieldMap, serializedFilterParam] = analyzeFilterArguments({ + let serializedFilterParam = filterValue; + let filterFieldMap = {}; + [filterFieldMap, serializedFilterParam] = analyzeFilterArguments({ filterValue, - typeFields, variableName, filterCypherParam, schemaType, @@ -2089,7 +2143,6 @@ const processFilterArgument = ({ }); translations = translateFilterArguments({ filterFieldMap, - typeFields, filterCypherParam, rootIsRelationType, variableName, @@ -2106,7 +2159,6 @@ const processFilterArgument = ({ const analyzeFilterArguments = ({ filterValue, - typeFields, variableName, filterCypherParam, schemaType, @@ -2115,7 +2167,6 @@ const analyzeFilterArguments = ({ return Object.entries(filterValue).reduce( ([filterFieldMap, serializedParams], [name, value]) => { const [serializedValue, fieldMap] = analyzeFilterArgument({ - field: typeFields[name], filterValue: value, filterValues: filterValue, fieldName: name, @@ -2135,7 +2186,6 @@ const analyzeFilterArguments = ({ const analyzeFilterArgument = ({ parentFieldName, - field, filterValue, fieldName, variableName, @@ -2144,24 +2194,32 @@ const analyzeFilterArgument = ({ schemaType, schema }) => { - const fieldType = field.type; - const innerFieldType = innerType(fieldType); - const typeName = innerFieldType.name; const parsedFilterName = parseFilterArgumentName(fieldName); let filterOperationField = parsedFilterName.name; let filterOperationType = parsedFilterName.type; // defaults let filterMapValue = true; let serializedFilterParam = filterValue; - if (isScalarType(innerFieldType) || isEnumType(innerFieldType)) { + let innerSchemaType = schemaType; + let typeName = schemaType.name; + if (filterOperationField !== 'OR' && filterOperationField !== 'AND') { + const schemaTypeFields = schemaType.getFields(); + const filterField = schemaTypeFields[filterOperationField]; + const filterFieldAst = filterField.astNode; + const filterType = filterFieldAst.type; + const innerFieldType = unwrapNamedType({ type: filterType }); + typeName = innerFieldType.name; + innerSchemaType = schema.getType(typeName); + } + if (isScalarType(innerSchemaType) || isEnumType(innerSchemaType)) { if (isExistentialFilter(filterOperationType, filterValue)) { serializedFilterParam = true; filterMapValue = null; } - } else if (isInputType(innerFieldType)) { - // check when filterSchemaType the same as schemaTypeField - const filterSchemaType = schema.getType(typeName); - const typeFields = filterSchemaType.getFields(); + } else if ( + isObjectType(innerSchemaType) || + isInterfaceType(innerSchemaType) + ) { if (fieldName === 'AND' || fieldName === 'OR') { // recursion [serializedFilterParam, filterMapValue] = analyzeNestedFilterArgument({ @@ -2172,7 +2230,6 @@ const analyzeFilterArgument = ({ schemaType, variableName, filterParam, - typeFields, schema }); } else { @@ -2210,7 +2267,13 @@ const analyzeFilterArgument = ({ if (isExistentialFilter(filterOperationType, filterValue)) { serializedFilterParam = true; filterMapValue = null; - } else if (isNeo4jTypeInput(typeName)) { + } else if ( + isTemporalType(typeName) || + isSpatialType(typeName) || + isSpatialDistanceInputType({ + filterOperationType + }) + ) { serializedFilterParam = serializeNeo4jTypeParam(filterValue); } else if (isRelation || isRelationType || isRelationTypeNode) { // recursion @@ -2224,7 +2287,6 @@ const analyzeFilterArgument = ({ schemaType: innerSchemaType, variableName, filterParam, - typeFields, schema } ); @@ -2242,7 +2304,6 @@ const analyzeNestedFilterArgument = ({ variableName, filterValue, filterParam, - typeFields, schema }) => { const isList = Array.isArray(filterValue); @@ -2258,7 +2319,6 @@ const analyzeNestedFilterArgument = ({ fieldName = deserializeFilterFieldName(fieldName); [serializedValue, valueFieldMap] = analyzeFilterArgument({ parentFieldName, - field: typeFields[fieldName], filterValue: value, filterValues: filter, fieldName, @@ -2329,7 +2389,6 @@ const deserializeFilterFieldName = name => { const translateFilterArguments = ({ filterFieldMap, - typeFields, filterCypherParam, variableName, rootIsRelationType, @@ -2341,7 +2400,6 @@ const translateFilterArguments = ({ // the filter field map uses serialized field names to allow for both field: {} and field: null name = deserializeFilterFieldName(name); const translation = translateFilterArgument({ - field: typeFields[name], filterParam: filterCypherParam, fieldName: name, filterValue: value, @@ -2363,7 +2421,6 @@ const translateFilterArgument = ({ parentParamPath, parentFieldName, isListFilterArgument, - field, filterValue, fieldName, rootIsRelationType, @@ -2373,19 +2430,26 @@ const translateFilterArgument = ({ schemaType, schema }) => { - const fieldType = field.type; - const innerFieldType = innerType(fieldType); - // get name of filter field type (ex: _PersonFilter) - const typeName = innerFieldType.name; + // parse field name into prefix (ex: name, company) and + // possible suffix identifying operation type (ex: _gt, _in) + const parsedFilterName = parseFilterArgumentName(fieldName); + const filterOperationField = parsedFilterName.name; + const filterOperationType = parsedFilterName.type; + let innerSchemaType = schemaType; + let typeName = schemaType.name; + if (filterOperationField !== 'OR' && filterOperationField !== 'AND') { + const schemaTypeFields = schemaType.getFields(); + const filterField = schemaTypeFields[filterOperationField]; + const filterFieldAst = filterField.astNode; + const filterType = filterFieldAst.type; + const innerFieldType = unwrapNamedType({ type: filterType }); + typeName = innerFieldType.name; + innerSchemaType = schema.getType(typeName); + } // build path for parameter data for current filter field const parameterPath = `${ parentParamPath ? parentParamPath : filterParam }.${fieldName}`; - // parse field name into prefix (ex: name, company) and - // possible suffix identifying operation type (ex: _gt, _in) - const parsedFilterName = parseFilterArgumentName(fieldName); - let filterOperationField = parsedFilterName.name; - let filterOperationType = parsedFilterName.type; // short-circuit evaluation: predicate used to skip a field // if processing a list of objects that possibly contain different arguments const nullFieldPredicate = decideNullSkippingPredicate({ @@ -2394,7 +2458,7 @@ const translateFilterArgument = ({ parentParamPath }); let translation = ''; - if (isScalarType(innerFieldType) || isEnumType(innerFieldType)) { + if (isScalarType(innerSchemaType) || isEnumType(innerSchemaType)) { translation = translateScalarFilter({ isListFilterArgument, filterOperationField, @@ -2407,7 +2471,10 @@ const translateFilterArgument = ({ filterParam, nullFieldPredicate }); - } else if (isInputType(innerFieldType)) { + } else if ( + isObjectType(innerSchemaType) || + isInterfaceType(innerSchemaType) + ) { translation = translateInputFilter({ rootIsRelationType, isListFilterArgument, @@ -2417,8 +2484,6 @@ const translateFilterArgument = ({ variableName, fieldName, filterParam, - typeName, - fieldType, schema, parentSchemaType, schemaType, @@ -2616,8 +2681,6 @@ const translateInputFilter = ({ variableName, fieldName, filterParam, - typeName, - fieldType, schema, parentSchemaType, schemaType, @@ -2626,9 +2689,6 @@ const translateInputFilter = ({ parentFieldName, nullFieldPredicate }) => { - // check when filterSchemaType the same as schemaTypeField - const filterSchemaType = schema.getType(typeName); - const typeFields = filterSchemaType.getFields(); if (fieldName === 'AND' || fieldName === 'OR') { return translateLogicalFilter({ filterValue, @@ -2637,7 +2697,6 @@ const translateInputFilter = ({ filterOperationField, fieldName, filterParam, - typeFields, schema, schemaType, parameterPath, @@ -2646,6 +2705,7 @@ const translateInputFilter = ({ } else { const schemaTypeField = schemaType.getFields()[filterOperationField]; const innerSchemaType = innerType(schemaTypeField.type); + const typeName = innerSchemaType.name; const isObjectTypeFilter = isObjectType(innerSchemaType); const isInterfaceTypeFilter = isInterfaceType(innerSchemaType); if (isObjectTypeFilter || isInterfaceTypeFilter) { @@ -2667,7 +2727,13 @@ const translateInputFilter = ({ innerSchemaType, filterOperationField }); - if (isNeo4jTypeInput(typeName)) { + if ( + isTemporalType(typeName) || + isSpatialType(typeName) || + isSpatialDistanceInputType({ + filterOperationType + }) + ) { return translateNeo4jTypeFilter({ typeName, isRelationTypeNode, @@ -2677,7 +2743,6 @@ const translateInputFilter = ({ filterOperationType, fieldName, filterParam, - fieldType, parameterPath, parentParamPath, isListFilterArgument, @@ -2700,8 +2765,6 @@ const translateInputFilter = ({ filterOperationType, fieldName, filterParam, - typeFields, - fieldType, schema, schemaType, innerSchemaType, @@ -2724,7 +2787,6 @@ const translateLogicalFilter = ({ filterOperationField, fieldName, filterParam, - typeFields, schema, schemaType, parameterPath, @@ -2743,7 +2805,7 @@ const translateLogicalFilter = ({ variableName, filterValue, filterParam, - typeFields, + // typeFields, schema }); const predicateListVariable = parameterPath; @@ -2778,8 +2840,6 @@ const translateRelationFilter = ({ filterOperationType, fieldName, filterParam, - typeFields, - fieldType, schema, schemaType, innerSchemaType, @@ -2814,13 +2874,15 @@ const translateRelationFilter = ({ isListFilterArgument }); } + let parentFilterOperationField = filterOperationField; + let parentFilterOperationType = filterOperationType; if (isReflexiveTypeDirectedField) { // causes the 'from' and 'to' fields on the payload of a reflexive // relation type to use the parent field name, ex: 'knows_some' // is used for 'from' and 'to' in 'knows_some: { from: {}, to: {} }' const parsedFilterName = parseFilterArgumentName(parentFieldName); - filterOperationField = parsedFilterName.name; - filterOperationType = parsedFilterName.type; + parentFilterOperationField = parsedFilterName.name; + parentFilterOperationType = parsedFilterName.type; } // build a list comprehension containing path pattern for related type const predicateListVariable = buildRelatedTypeListComprehension({ @@ -2836,11 +2898,13 @@ const translateRelationFilter = ({ const rootPredicateFunction = decidePredicateFunction({ isRelationTypeNode, - filterOperationField, - filterOperationType + filterOperationField: parentFilterOperationField, + filterOperationType: parentFilterOperationType }); + return buildRelationPredicate({ rootIsRelationType, + parentFieldName, isRelationType, isListFilterArgument, isReflexiveRelationType, @@ -2850,11 +2914,9 @@ const translateRelationFilter = ({ schemaType, innerSchemaType, fieldName, - fieldType, filterOperationType, filterValue, filterParam, - typeFields, schema, parameterPath, nullFieldPredicate, @@ -2952,6 +3014,7 @@ const decideRelationTypeDirection = (schemaType, relationTypeDirective) => { const buildRelationPredicate = ({ rootIsRelationType, + parentFieldName, isRelationType, isReflexiveRelationType, isReflexiveTypeDirectedField, @@ -2961,11 +3024,9 @@ const buildRelationPredicate = ({ schemaType, innerSchemaType, fieldName, - fieldType, filterOperationType, filterValue, filterParam, - typeFields, schema, parameterPath, nullFieldPredicate, @@ -2973,8 +3034,9 @@ const buildRelationPredicate = ({ predicateListVariable, rootPredicateFunction }) => { + let isRelationList = + filterOperationType === 'in' || filterOperationType === 'not_in'; let relationVariable = buildRelationVariable(thisType, relatedType); - const isRelationList = isListType(fieldType); let variableName = relatedType.toLowerCase(); let listVariable = parameterPath; if (rootIsRelationType || isRelationType) { @@ -3003,7 +3065,6 @@ const buildRelationPredicate = ({ isRelationType, filterValue, filterParam, - typeFields, schema }); if (isRelationList) { @@ -3157,7 +3218,6 @@ const buildFilterPredicates = ({ listVariable, filterValue, filterParam, - typeFields, schema, isListFilterArgument }) => { @@ -3165,7 +3225,6 @@ const buildFilterPredicates = ({ .reduce((predicates, [name, value]) => { name = deserializeFilterFieldName(name); const predicate = translateFilterArgument({ - field: typeFields[name], parentParamPath: listVariable, fieldName: name, filterValue: value, @@ -3194,57 +3253,72 @@ const translateNeo4jTypeFilter = ({ filterOperationType, fieldName, filterParam, - fieldType, parameterPath, parentParamPath, isListFilterArgument, nullFieldPredicate }) => { let predicate = ''; + let cypherTypeConstructor = ''; if ( - isTemporalInputType(typeName) || - isSpatialInputType(typeName) || - isSpatialDistanceInputType(typeName) + !isSpatialDistanceInputType({ + filterOperationType + }) ) { - const cypherTypeConstructor = decideNeo4jTypeConstructor(typeName); - const safeVariableName = safeVar(variableName); - let propertyPath = `${safeVariableName}.${filterOperationField}`; - if (isExistentialFilter(filterOperationType, filterValue)) { - return translateNullFilter({ - filterOperationField, - filterOperationType, - propertyPath, - filterParam, - parentParamPath, - isListFilterArgument - }); + switch (typeName) { + case '_Neo4jTime': + cypherTypeConstructor = 'time'; + break; + case '_Neo4jDate': + cypherTypeConstructor = 'date'; + break; + case '_Neo4jDateTime': + cypherTypeConstructor = 'datetime'; + break; + case '_Neo4jLocalTime': + cypherTypeConstructor = 'localtime'; + break; + case '_Neo4jLocalDateTime': + cypherTypeConstructor = 'localdatetime'; + break; + case '_Neo4jPoint': + cypherTypeConstructor = 'point'; + break; } - const rootPredicateFunction = decidePredicateFunction({ - isRelationTypeNode, - filterOperationField, - filterOperationType - }); - predicate = buildNeo4jTypePredicate({ - typeName, - fieldName, - fieldType, - filterValue, + } + const safeVariableName = safeVar(variableName); + let propertyPath = `${safeVariableName}.${filterOperationField}`; + if (isExistentialFilter(filterOperationType, filterValue)) { + return translateNullFilter({ filterOperationField, filterOperationType, - parameterPath, - variableName, - nullFieldPredicate, - rootPredicateFunction, - cypherTypeConstructor + propertyPath, + filterParam, + parentParamPath, + isListFilterArgument }); } + const rootPredicateFunction = decidePredicateFunction({ + isRelationTypeNode, + filterOperationField, + filterOperationType + }); + predicate = buildNeo4jTypePredicate({ + typeName, + fieldName, + filterOperationField, + filterOperationType, + parameterPath, + variableName, + nullFieldPredicate, + rootPredicateFunction, + cypherTypeConstructor + }); return predicate; }; const buildNeo4jTypePredicate = ({ - typeName, fieldName, - fieldType, filterOperationField, filterOperationType, parameterPath, @@ -3253,8 +3327,9 @@ const buildNeo4jTypePredicate = ({ rootPredicateFunction, cypherTypeConstructor }) => { + const isListFilterArgument = + filterOperationType === 'in' || filterOperationType === 'not_in'; // ex: project -> person_filter_project - const isListFilterArgument = isListType(fieldType); let listVariable = parameterPath; // ex: $filter.datetime_in -> _datetime_in if (isListFilterArgument) listVariable = `_${fieldName}`; @@ -3266,7 +3341,11 @@ const buildNeo4jTypePredicate = ({ isListFilterArgument, parameterPath }); - if (isSpatialDistanceInputType(typeName)) { + if ( + isSpatialDistanceInputType({ + filterOperationType + }) + ) { listVariable = `${listVariable}.distance`; } let translation = `(${nullFieldPredicate}${operatorExpression} ${cypherTypeConstructor}(${listVariable}))`; diff --git a/src/utils.js b/src/utils.js index 7c6bb71a..318073f9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,6 +5,7 @@ import { Neo4jTypeName } from './augment/types/types'; import { SpatialType } from './augment/types/spatial'; import { unwrapNamedType } from './augment/fields'; import { Neo4jTypeFormatted } from './augment/types/types'; +import { getFederatedOperationData } from './federation'; function parseArg(arg, variableValues) { switch (arg.value.kind) { @@ -97,7 +98,9 @@ export function cypherDirectiveArgs( cypherParams, schemaType, resolveInfo, - paramIndex + paramIndex, + isFederatedOperation, + context ) { // Get any default arguments or an empty object const defaultArgs = getDefaultArguments(headSelection.name.value, schemaType); @@ -105,6 +108,17 @@ export function cypherDirectiveArgs( let args = [`this: ${variable}`]; // If cypherParams are provided, add the parameter if (cypherParams) args.push(`cypherParams: $cypherParams`); + let federatedOperationParams = {}; + if (isFederatedOperation) { + const { requiredData, params } = getFederatedOperationData({ context }); + federatedOperationParams = { + ...requiredData, + ...params + }; + Object.keys(federatedOperationParams).forEach(name => { + args.push(`${name}: $${name}`); + }); + } // Parse field argument values const queryArgs = parseArgs( headSelection.arguments, @@ -113,7 +127,11 @@ export function cypherDirectiveArgs( // Add arguments that have default values, if no value is provided Object.keys(defaultArgs).forEach(e => { // Use only if default value exists and no value has been provided - if (defaultArgs[e] !== undefined && queryArgs[e] === undefined) { + if ( + defaultArgs[e] !== undefined && + queryArgs[e] === undefined && + federatedOperationParams[e] === undefined + ) { // Values are inlined const inlineDefaultValue = JSON.stringify(defaultArgs[e]); args.push(`${e}: ${inlineDefaultValue}`); @@ -121,7 +139,10 @@ export function cypherDirectiveArgs( }); // Add arguments that have provided values Object.keys(queryArgs).forEach(e => { - if (queryArgs[e] !== undefined) { + if ( + queryArgs[e] !== undefined && + federatedOperationParams[e] === undefined + ) { // Use only if value exists args.push(`${e}: $${paramIndex}_${e}`); } @@ -367,8 +388,8 @@ export const possiblySetFirstId = ({ args, statements = [], params }) => { return statements; }; -export const getQueryArguments = resolveInfo => { - if (resolveInfo.fieldName === '_entities') return []; +export const getQueryArguments = (resolveInfo, isFederatedOperation) => { + if (resolveInfo.fieldName === '_entities' || isFederatedOperation) return []; return resolveInfo.schema.getQueryType().getFields()[resolveInfo.fieldName] .astNode.arguments; }; @@ -602,8 +623,8 @@ export const getFieldDirective = (field, directive) => { ); }; -export const getQueryCypherDirective = resolveInfo => { - if (resolveInfo.fieldName === '_entities') return; +export const getQueryCypherDirective = (resolveInfo, isFederatedOperation) => { + if (resolveInfo.fieldName === '_entities' || isFederatedOperation) return; return resolveInfo.schema .getQueryType() .getFields() @@ -946,8 +967,18 @@ export const isSpatialField = (schemaType, name) => { export const isSpatialInputType = name => name === '_Neo4jPointInput'; -export const isSpatialDistanceInputType = name => - name === `${Neo4jTypeName}${SpatialType.POINT}DistanceFilter`; +export const isSpatialDistanceInputType = ({ filterOperationType = '' }) => { + switch (filterOperationType) { + case 'distance': + case 'distance_lt': + case 'distance_lte': + case 'distance_gt': + case 'distance_gte': + return true; + default: + return false; + } +}; export const decideNeo4jTypeConstructor = typeName => { switch (typeName) { @@ -1064,8 +1095,11 @@ export const getInterfaceDerivedTypeNames = (schema, interfaceName) => { const implementingTypeMap = schema._implementations ? schema._implementations[interfaceName] : {}; - const implementingTypes = Object.values(implementingTypeMap).map( - type => type.name - ); + let implementingTypes = []; + if (implementingTypeMap) { + implementingTypes = Object.values(implementingTypeMap).map( + type => type.name + ); + } return implementingTypes.sort(); }; diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 7568ec8e..2e3e44b7 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -32,8 +32,6 @@ type Mutation { computedTemporal: DateTime @cypher(statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }") computedSpatial: Point @cypher(statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }") customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") - CustomCamera: Camera @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera") - CustomCameras: [Camera] @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]") CreateNewCamera(id: ID, type: String, make: String, weight: Int, features: [String]): NewCamera CreateActor(userId: ID, name: String): Actor computedMovieSearch: [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index a649468b..cacf925d 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -1,4 +1,6 @@ -export const testSchema = /* GraphQL */ ` +import { gql } from 'apollo-server'; + +export const testSchema = ` type Movie @additionalLabels( labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] @@ -52,6 +54,9 @@ export const testSchema = /* GraphQL */ ` imdbRatings: [Float] releases: [DateTime] customField: String @neo4j_ignore + } + + extend type Movie { currentUserId(strArg: String): String @cypher( statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" @@ -62,6 +67,11 @@ export const testSchema = /* GraphQL */ ` @relation(name: "INTERFACE_NO_SCALARS", direction: OUT) } + extend type Movie @hasRole(roles: [admin]) { + extensionScalar: String + extensionNode: [Genre] @relation(name: "IN_GENRE", direction: "OUT") + } + type Genre { _id: String! name: String @@ -83,6 +93,10 @@ export const testSchema = /* GraphQL */ ` name: String } + extend interface Person { + extensionScalar: String + } + enum _PersonOrdering { userId_asc userId_desc @@ -115,13 +129,16 @@ export const testSchema = /* GraphQL */ ` name_not_ends_with: String } - type Actor implements Person { + type Actor { userId: ID! name: String movies: [Movie] @relation(name: "ACTED_IN", direction: "OUT") knows: [Person] @relation(name: "KNOWS", direction: "OUT") + extensionScalar: String } + extend type Actor implements Person + type User implements Person { userId: ID! name: String @@ -151,6 +168,7 @@ export const testSchema = /* GraphQL */ ` movieSearch: [MovieSearch] computedMovieSearch: [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") + extensionScalar: String } type FriendOf @relation { @@ -191,6 +209,9 @@ export const testSchema = /* GraphQL */ ` enum BookGenre { Mystery Science + } + + extend enum BookGenre { Math } @@ -263,6 +284,9 @@ export const testSchema = /* GraphQL */ ` ): [InterfaceNoScalars] CustomCameras: [Camera] @cypher(statement: "MATCH (c:Camera) RETURN c") CustomCamera: Camera @cypher(statement: "MATCH (c:Camera) RETURN c") + } + + extend type QueryA { MovieSearch(first: Int): [MovieSearch] computedMovieSearch: [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") @@ -288,6 +312,11 @@ export const testSchema = /* GraphQL */ ` customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") testPublish: Boolean @neo4j_ignore + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") + } + + extend type Mutation { CustomCamera: Camera @cypher( statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera" @@ -296,8 +325,6 @@ export const testSchema = /* GraphQL */ ` @cypher( statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]" ) - computedMovieSearch: [MovieSearch] - @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") } type currentUserId { @@ -339,10 +366,16 @@ export const testSchema = /* GraphQL */ ` scalar LocalTime scalar LocalDateTime + extend scalar Time @neo4j_ignore + input strInput { strArg: String } + extend input strInput { + extensionArg: String + } + enum Role { reader user @@ -471,7 +504,9 @@ export const testSchema = /* GraphQL */ ` @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") } - union MovieSearch = Movie | Genre | Book | Actor | OldCamera + union MovieSearch = Movie | Genre | Book + + extend union MovieSearch = Actor | OldCamera type CameraMan implements Person { userId: ID! @@ -483,6 +518,7 @@ export const testSchema = /* GraphQL */ ` ) cameras: [Camera!]! @relation(name: "cameras", direction: "OUT") cameraBuddy: Person @relation(name: "cameraBuddy", direction: "OUT") + extensionScalar: String } type SubscriptionC { @@ -491,7 +527,10 @@ export const testSchema = /* GraphQL */ ` schema { query: QueryA - mutation: Mutation subscription: SubscriptionC } + + extend schema { + mutation: Mutation + } `; diff --git a/test/integration/gateway.test.js b/test/integration/gateway.test.js new file mode 100644 index 00000000..0fb3364b --- /dev/null +++ b/test/integration/gateway.test.js @@ -0,0 +1,573 @@ +import test from 'ava'; + +import { ApolloClient } from 'apollo-client'; +import { HttpLink } from 'apollo-link-http'; +import { InMemoryCache } from 'apollo-cache-inmemory'; + +import gql from 'graphql-tag'; +import fetch from 'node-fetch'; + +let client; + +test.before(async t => { + client = new ApolloClient({ + link: new HttpLink({ uri: 'http://localhost:4000', fetch: fetch }), + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + fetchPolicy: 'no-cache', + errorPolicy: 'ignore' + }, + query: { + fetchPolicy: 'no-cache', + errorPolicy: 'all' + } + } + }); + await client + .mutate({ + mutation: gql` + mutation { + MergeSeedData + } + ` + }) + .then(data => { + return data; + }) + .catch(error => { + t.fail(error.message); + }); +}); + +test.after(async t => { + await client + .mutate({ + mutation: gql` + mutation { + DeleteSeedData + } + ` + }) + .then(data => { + return data; + }) + .catch(error => { + t.fail(error.message); + }); +}); + +test.serial( + 'Query for merged test data (reviews -> ((products -> inventory) + accounts))', + async t => { + t.plan(1); + + const expected = { + data: { + Review: [ + { + id: '1', + body: 'Love it!', + product: { + upc: '1', + name: 'Table', + price: 899, + weight: 100, + shippingEstimate: 50, + inStock: true, + metrics: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + objectCompoundKey: { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + }, + listCompoundKey: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + __typename: 'Product' + }, + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + }, + { + id: '2', + body: 'Too expensive.', + product: { + upc: '2', + name: 'Couch', + price: 1299, + weight: 1000, + shippingEstimate: 0, + inStock: false, + metrics: [], + objectCompoundKey: null, + listCompoundKey: [], + __typename: 'Product' + }, + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + }, + { + id: '3', + body: 'Could be better.', + product: { + upc: '3', + name: 'Chair', + price: 54, + weight: 50, + shippingEstimate: 25, + inStock: true, + metrics: [], + objectCompoundKey: null, + listCompoundKey: [], + __typename: 'Product' + }, + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + }, + { + id: '4', + body: 'Prefer something else.', + product: { + upc: '1', + name: 'Table', + price: 899, + weight: 100, + shippingEstimate: 50, + inStock: true, + metrics: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + objectCompoundKey: { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + }, + listCompoundKey: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + __typename: 'Product' + }, + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + } + ] + } + }; + + await client + .query({ + query: gql` + query { + Review { + id + body + product { + upc + name + price + weight + shippingEstimate + inStock + metrics { + id + metric + data + } + objectCompoundKey { + id + metric + data + } + listCompoundKey { + id + metric + data + } + } + author { + id + name + username + numberOfReviews + } + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Field arguments with service path: (products -> (inventory + (reviews -> accounts)))', + async t => { + t.plan(1); + + const expected = { + data: { + Product: [ + { + upc: '3', + name: 'Chair', + weight: 50, + price: 54, + inStock: true, + shippingEstimate: 25, + reviews: [ + { + id: '3', + body: 'Could be better.', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + } + ], + metrics: [], + listCompoundKey: [], + __typename: 'Product' + }, + { + upc: '2', + name: 'Couch', + weight: 1000, + price: 1299, + inStock: false, + shippingEstimate: 0, + reviews: [ + { + id: '2', + body: 'Too expensive.', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + } + ], + metrics: [], + listCompoundKey: [], + __typename: 'Product' + }, + { + upc: '1', + name: 'Table', + weight: 100, + price: 899, + inStock: true, + shippingEstimate: 50, + reviews: [ + { + id: '4', + body: 'Prefer something else.', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + }, + { + id: '1', + body: 'Love it!', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + numberOfReviews: 2, + __typename: 'Account' + }, + __typename: 'Review' + } + ], + metrics: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + listCompoundKey: [ + { + id: '100', + metric: 1, + data: 2, + __typename: 'Metric' + } + ], + __typename: 'Product' + } + ] + } + }; + + await client + .query({ + query: gql` + query { + Product(orderBy: upc_desc) { + upc + name + weight + price + inStock + shippingEstimate + reviews( + first: 2 + filter: { id_in: ["1", "2", "3", "4"] } + orderBy: id_desc + ) { + id + body + author { + id + name + username + numberOfReviews + } + } + metrics { + id + metric + data + } + listCompoundKey { + id + metric + data + } + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Unselected @requires fields with service path: (accounts -> (reviews -> (accounts + (products + inventory))))', + async t => { + t.plan(1); + + const expected = { + data: { + Account: [ + { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + reviews: [ + { + id: '1', + body: 'Love it!', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + __typename: 'Account' + }, + product: { + upc: '1', + name: 'Table', + inStock: true, + shippingEstimate: 50, + metrics: [ + { + id: '100', + data: 2, + __typename: 'Metric' + } + ], + __typename: 'Product' + }, + __typename: 'Review' + }, + { + id: '2', + body: 'Too expensive.', + author: { + id: '1', + name: 'Ada Lovelace', + username: '@ada', + __typename: 'Account' + }, + product: { + upc: '2', + name: 'Couch', + inStock: false, + shippingEstimate: 0, + metrics: [], + __typename: 'Product' + }, + __typename: 'Review' + } + ], + numberOfReviews: 2, + __typename: 'Account' + }, + { + id: '2', + name: 'Alan Turing', + username: '@complete', + reviews: [ + { + id: '3', + body: 'Could be better.', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + __typename: 'Account' + }, + product: { + upc: '3', + name: 'Chair', + inStock: true, + shippingEstimate: 25, + metrics: [], + __typename: 'Product' + }, + __typename: 'Review' + }, + { + id: '4', + body: 'Prefer something else.', + author: { + id: '2', + name: 'Alan Turing', + username: '@complete', + __typename: 'Account' + }, + product: { + upc: '1', + name: 'Table', + inStock: true, + shippingEstimate: 50, + metrics: [ + { + id: '100', + data: 2, + __typename: 'Metric' + } + ], + __typename: 'Product' + }, + __typename: 'Review' + } + ], + numberOfReviews: 2, + __typename: 'Account' + } + ] + } + }; + + await client + .query({ + query: gql` + query { + Account { + id + name + username + reviews(orderBy: id_asc) { + id + body + author { + id + name + username + } + product { + upc + name + inStock + shippingEstimate + metrics { + id + data + } + } + } + numberOfReviews + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); + } +); diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 36ba604b..58e3ae71 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -1,13 +1,16 @@ import test from 'ava'; -import { parse, print } from 'graphql'; +import { parse, print, Kind } from 'graphql'; import { printSchemaDocument } from '../../src/augment/augment'; import { makeAugmentedSchema } from '../../src/index'; import { testSchema } from '../helpers/testSchema'; -import { Kind } from 'graphql/language'; +import { gql } from 'apollo-server'; test.cb('Test augmented schema', t => { + const parseTypeDefs = gql` + ${testSchema} + `; const sourceSchema = makeAugmentedSchema({ - typeDefs: testSchema, + typeDefs: parseTypeDefs, config: { auth: true } @@ -151,9 +154,6 @@ test.cb('Test augmented schema', t => { orderBy: [_CameraOrdering] ): [Camera] @cypher(statement: "MATCH (c:Camera) RETURN c") CustomCamera: Camera @cypher(statement: "MATCH (c:Camera) RETURN c") - MovieSearch(first: Int, offset: Int): [MovieSearch] - computedMovieSearch(first: Int, offset: Int): [MovieSearch] - @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") Genre( _id: String name: String @@ -165,6 +165,7 @@ test.cb('Test augmented schema', t => { Person( userId: ID name: String + extensionScalar: String _id: String first: Int offset: Int @@ -174,6 +175,7 @@ test.cb('Test augmented schema', t => { Actor( userId: ID name: String + extensionScalar: String _id: String first: Int offset: Int @@ -239,6 +241,7 @@ test.cb('Test augmented schema', t => { CameraMan( userId: ID name: String + extensionScalar: String _id: String first: Int offset: Int @@ -247,6 +250,12 @@ test.cb('Test augmented schema', t => { ): [CameraMan] @hasScope(scopes: ["CameraMan: Read"]) } + extend type QueryA { + MovieSearch(first: Int, offset: Int): [MovieSearch] + computedMovieSearch(first: Int, offset: Int): [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") + } + input _Neo4jDateTimeInput { year: Int month: Int @@ -290,6 +299,8 @@ test.cb('Test augmented schema', t => { scaleRatingFloat_desc currentUserId_asc currentUserId_desc + extensionScalar_asc + extensionScalar_desc } input _MovieFilter { @@ -420,6 +431,24 @@ test.cb('Test augmented schema', t => { interfaceNoScalars_none: _InterfaceNoScalarsFilter interfaceNoScalars_single: _InterfaceNoScalarsFilter interfaceNoScalars_every: _InterfaceNoScalarsFilter + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String + extensionNode: _GenreFilter + extensionNode_not: _GenreFilter + extensionNode_in: [_GenreFilter!] + extensionNode_not_in: [_GenreFilter!] + extensionNode_some: _GenreFilter + extensionNode_none: _GenreFilter + extensionNode_single: _GenreFilter + extensionNode_every: _GenreFilter } input _GenreFilter { @@ -484,6 +513,16 @@ test.cb('Test augmented schema', t => { knows_none: _PersonFilter knows_single: _PersonFilter knows_every: _PersonFilter + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String } input _StateFilter { @@ -650,6 +689,16 @@ test.cb('Test augmented schema', t => { favorites_none: _MovieFilter favorites_single: _MovieFilter favorites_every: _MovieFilter + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String } input _UserRatedFilter { @@ -858,6 +907,9 @@ test.cb('Test augmented schema', t => { imdbRatings: [Float] releases: [_Neo4jDateTime] customField: String @neo4j_ignore + } + + extend type Movie { currentUserId(strArg: String): String @cypher( statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" @@ -871,6 +923,16 @@ test.cb('Test augmented schema', t => { @relation(name: "INTERFACE_NO_SCALARS", direction: OUT) } + extend type Movie @hasRole(roles: [admin]) { + extensionScalar: String + extensionNode( + first: Int + offset: Int + orderBy: [_GenreOrdering] + filter: _GenreFilter + ): [Genre] @relation(name: "IN_GENRE", direction: "OUT") + } + type _Neo4jDateTime { year: Int month: Int @@ -910,11 +972,13 @@ test.cb('Test augmented schema', t => { userId_desc name_asc name_desc + extensionScalar_asc + extensionScalar_desc _id_asc _id_desc } - type Actor implements Person { + type Actor { userId: ID! name: String movies( @@ -929,14 +993,21 @@ test.cb('Test augmented schema', t => { orderBy: [_PersonOrdering] filter: _PersonFilter ): [Person] @relation(name: "KNOWS", direction: "OUT") + extensionScalar: String _id: String } + extend type Actor implements Person + interface Person { userId: ID! name: String } + extend interface Person { + extensionScalar: String + } + type State { customField: String @neo4j_ignore name: String! @@ -1028,6 +1099,7 @@ test.cb('Test augmented schema', t => { movieSearch(first: Int, offset: Int): [MovieSearch] computedMovieSearch(first: Int, offset: Int): [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") + extensionScalar: String _id: String } @@ -1035,6 +1107,10 @@ test.cb('Test augmented schema', t => { strArg: String } + extend input strInput { + extensionArg: String + } + type _UserRated @relation(name: "RATED", from: "User", to: "Movie") { currentUserId(strArg: String): String @cypher( @@ -1119,6 +1195,8 @@ test.cb('Test augmented schema', t => { name_desc currentUserId_asc currentUserId_desc + extensionScalar_asc + extensionScalar_desc _id_asc _id_desc } @@ -1142,6 +1220,9 @@ test.cb('Test augmented schema', t => { enum BookGenre { Mystery Science + } + + extend enum BookGenre { Math } @@ -1617,6 +1698,8 @@ test.cb('Test augmented schema', t => { userId_desc name_asc name_desc + extensionScalar_asc + extensionScalar_desc _id_asc _id_desc } @@ -1660,9 +1743,21 @@ test.cb('Test augmented schema', t => { cameraBuddy_not: _PersonFilter cameraBuddy_in: [_PersonFilter!] cameraBuddy_not_in: [_PersonFilter!] + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String } - union MovieSearch = Movie | Genre | Book | Actor | OldCamera + union MovieSearch = Movie | Genre | Book + + extend union MovieSearch = Actor | OldCamera type CameraMan implements Person { userId: ID! @@ -1685,6 +1780,7 @@ test.cb('Test augmented schema', t => { ): [Camera!]! @relation(name: "cameras", direction: "OUT") cameraBuddy(filter: _PersonFilter): Person @relation(name: "cameraBuddy", direction: "OUT") + extensionScalar: String _id: String } @@ -1738,16 +1834,26 @@ test.cb('Test augmented schema', t => { customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") testPublish: Boolean @neo4j_ignore - CustomCamera: Camera - @cypher( - statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera" - ) - CustomCameras: [Camera] - @cypher( - statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]" - ) computedMovieSearch: [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") + AddMovieExtensionNode( + from: _MovieInput! + to: _GenreInput! + ): _AddMovieExtensionNodePayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Create", "Genre: Create"]) + RemoveMovieExtensionNode( + from: _MovieInput! + to: _GenreInput! + ): _RemoveMovieExtensionNodePayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + MergeMovieExtensionNode( + from: _MovieInput! + to: _GenreInput! + ): _MergeMovieExtensionNodePayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Merge", "Genre: Merge"]) AddMovieGenres( from: _MovieInput! to: _GenreInput! @@ -1845,6 +1951,7 @@ test.cb('Test augmented schema', t => { titles: [String] imdbRatings: [Float] releases: [_Neo4jDateTimeInput] + extensionScalar: String ): Movie @hasScope(scopes: ["Movie: Create"]) UpdateMovie( movieId: ID! @@ -1862,6 +1969,7 @@ test.cb('Test augmented schema', t => { titles: [String] imdbRatings: [Float] releases: [_Neo4jDateTimeInput] + extensionScalar: String ): Movie @hasScope(scopes: ["Movie: Update"]) DeleteMovie(movieId: ID!): Movie @hasScope(scopes: ["Movie: Delete"]) MergeMovie( @@ -1880,6 +1988,7 @@ test.cb('Test augmented schema', t => { titles: [String] imdbRatings: [Float] releases: [_Neo4jDateTimeInput] + extensionScalar: String ): Movie @hasScope(scopes: ["Movie: Merge"]) AddGenreMovies( from: _MovieInput! @@ -1939,12 +2048,12 @@ test.cb('Test augmented schema', t => { ): _MergeActorKnowsPayload @MutationMeta(relationship: "KNOWS", from: "Actor", to: "Person") @hasScope(scopes: ["Actor: Merge", "Person: Merge"]) - CreateActor(userId: ID, name: String): Actor + CreateActor(userId: ID, name: String, extensionScalar: String): Actor @hasScope(scopes: ["Actor: Create"]) - UpdateActor(userId: ID!, name: String): Actor + UpdateActor(userId: ID!, name: String, extensionScalar: String): Actor @hasScope(scopes: ["Actor: Update"]) DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) - MergeActor(userId: ID!, name: String): Actor + MergeActor(userId: ID!, name: String, extensionScalar: String): Actor @hasScope(scopes: ["Actor: Merge"]) AddUserRated( from: _UserInput! @@ -2018,12 +2127,12 @@ test.cb('Test augmented schema', t => { ): _MergeUserFavoritesPayload @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") @hasScope(scopes: ["User: Merge", "Movie: Merge"]) - CreateUser(userId: ID, name: String): User + CreateUser(userId: ID, name: String, extensionScalar: String): User @hasScope(scopes: ["User: Create"]) - UpdateUser(userId: ID!, name: String): User + UpdateUser(userId: ID!, name: String, extensionScalar: String): User @hasScope(scopes: ["User: Update"]) DeleteUser(userId: ID!): User @hasScope(scopes: ["User: Delete"]) - MergeUser(userId: ID!, name: String): User + MergeUser(userId: ID!, name: String, extensionScalar: String): User @hasScope(scopes: ["User: Merge"]) CreateBook(genre: BookGenre): Book @hasScope(scopes: ["Book: Create"]) DeleteBook(genre: BookGenre!): Book @hasScope(scopes: ["Book: Delete"]) @@ -2328,16 +2437,35 @@ test.cb('Test augmented schema', t => { to: "Person" ) @hasScope(scopes: ["CameraMan: Merge", "Person: Merge"]) - CreateCameraMan(userId: ID, name: String): CameraMan - @hasScope(scopes: ["CameraMan: Create"]) - UpdateCameraMan(userId: ID!, name: String): CameraMan - @hasScope(scopes: ["CameraMan: Update"]) + CreateCameraMan( + userId: ID + name: String + extensionScalar: String + ): CameraMan @hasScope(scopes: ["CameraMan: Create"]) + UpdateCameraMan( + userId: ID! + name: String + extensionScalar: String + ): CameraMan @hasScope(scopes: ["CameraMan: Update"]) DeleteCameraMan(userId: ID!): CameraMan @hasScope(scopes: ["CameraMan: Delete"]) - MergeCameraMan(userId: ID!, name: String): CameraMan - @hasScope(scopes: ["CameraMan: Merge"]) + MergeCameraMan( + userId: ID! + name: String + extensionScalar: String + ): CameraMan @hasScope(scopes: ["CameraMan: Merge"]) } + extend type Mutation { + CustomCamera: Camera + @cypher( + statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera" + ) + CustomCameras: [Camera] + @cypher( + statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]" + ) + } input _MovieInput { movieId: ID! } @@ -2346,6 +2474,24 @@ test.cb('Test augmented schema', t => { name: String! } + type _AddMovieExtensionNodePayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _RemoveMovieExtensionNodePayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _MergeMovieExtensionNodePayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + type _AddMovieGenresPayload @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { from: Movie @@ -2923,6 +3069,8 @@ test.cb('Test augmented schema', t => { scalar LocalDateTime + extend scalar Time @neo4j_ignore + enum Role { reader user @@ -3021,9 +3169,6 @@ test.cb('Test augmented schema', t => { const compareSchema = ({ test, sourceSchema = {}, expectedSchema = {} }) => { const expectedDefinitions = parse(expectedSchema).definitions; - // printSchema is no longer used here, as it simplifies out the schema type and all - // directive instances. printSchemaDocument does not simplify anything out, as it uses - // the graphql print function instead, along with the regeneration of the schema type const printedSourceSchema = printSchemaDocument({ schema: sourceSchema }); const augmentedDefinitions = parse(printedSourceSchema).definitions; augmentedDefinitions.forEach(augmentedDefinition => { @@ -3039,7 +3184,29 @@ const compareSchema = ({ test, sourceSchema = {}, expectedSchema = {} }) => { expectedDefinition = expectedDefinitions.find(definition => { if (definition.name) { if (definition.name.value === augmentedDefinition.name.value) { - return definition; + if (definition.kind === augmentedDefinition.kind) { + if ( + definition.kind === Kind.OBJECT_TYPE_EXTENSION + // definition.kind === Kind.INTERFACE_TYPE_EXTENSION || + // definition.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION + ) { + if ( + definition.fields.length && + augmentedDefinition.fields.length + ) { + if ( + definition.fields[0].name.value === + augmentedDefinition.fields[0].name.value + ) { + return definition; + } + } else { + return definition; + } + } else { + return definition; + } + } } } });