From df1d953ce1b4b67357f5e6f1b75e2bfd3b9e1a43 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 28 Dec 2020 21:25:32 +0200 Subject: [PATCH] code-first stitched schemas (#11) * initial commit accounts is now world's first code-first stitched schema using graphql-js to do: = convert a service to TypeGraphQL = convert a service to nexus = convert a service to... = simplify type merging configuration to only use simple keys -- as this example is not meant to showcase the more coplex options = fix the awkwardness with `{ stitchingDirectives: directive } = stitchingDirectives()` * fix name clash based on latest alpha * rename folder * upgrade dependencies * use latest graphql-tools alpha export names * nexus supprt * remove copied text * add TypeGraphQL example * update README * integrate into table of contents * fix notes * add missing dep * update deps * fix TypeGraphQL example * formatting * simplify * finesse --- README.md | 4 + code-first-schemas/README.md | 91 +++++++++++++++++ code-first-schemas/index.js | 69 +++++++++++++ .../lib/make_remote_executor.js | 14 +++ code-first-schemas/lib/make_server.js | 8 ++ code-first-schemas/lib/not_found_error.js | 6 ++ code-first-schemas/lib/read_file_sync.js | 6 ++ code-first-schemas/package.json | 33 +++++++ code-first-schemas/services/accounts/index.js | 2 + .../services/accounts/schema.js | 74 ++++++++++++++ .../services/inventory/index.js | 2 + .../services/inventory/schema.js | 97 +++++++++++++++++++ code-first-schemas/services/products/index.ts | 6 ++ .../services/products/schema.ts | 65 +++++++++++++ code-first-schemas/services/reviews/index.js | 2 + .../services/reviews/schema.graphql | 36 +++++++ code-first-schemas/services/reviews/schema.js | 42 ++++++++ code-first-schemas/tsconfig.json | 9 ++ 18 files changed, 566 insertions(+) create mode 100644 code-first-schemas/README.md create mode 100644 code-first-schemas/index.js create mode 100644 code-first-schemas/lib/make_remote_executor.js create mode 100644 code-first-schemas/lib/make_server.js create mode 100644 code-first-schemas/lib/not_found_error.js create mode 100644 code-first-schemas/lib/read_file_sync.js create mode 100644 code-first-schemas/package.json create mode 100644 code-first-schemas/services/accounts/index.js create mode 100644 code-first-schemas/services/accounts/schema.js create mode 100644 code-first-schemas/services/inventory/index.js create mode 100644 code-first-schemas/services/inventory/schema.js create mode 100644 code-first-schemas/services/products/index.ts create mode 100644 code-first-schemas/services/products/schema.ts create mode 100644 code-first-schemas/services/reviews/index.js create mode 100644 code-first-schemas/services/reviews/schema.graphql create mode 100644 code-first-schemas/services/reviews/schema.js create mode 100644 code-first-schemas/tsconfig.json diff --git a/README.md b/README.md index 16a03be..8c38eca 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ Guided examples of [Schema Stitching](https://www.graphql-tools.com/docs/stitch- - Integrating Apollo Federation services into a stitched schema. - Fetching and parsing Federation SDLs. +- **[Code-first schemas](./code-first-schemas)** + + - Integrating schemas created with `graphql-js`, `nexus`, and `type-graphql` into a stitched schema. + ### Appendices - [What is Array Batching?](https://github.com/gmac/schema-stitching-demos/wiki/Batching-Arrays-and-Queries#what-is-array-batching) diff --git a/code-first-schemas/README.md b/code-first-schemas/README.md new file mode 100644 index 0000000..fbc357e --- /dev/null +++ b/code-first-schemas/README.md @@ -0,0 +1,91 @@ +# Stitching Directives with Code-First Schemas + +This example demonstrates the use of stitching directives to configure type merging, similar to the prior example, but uses code-first schemas instead of SDL. + +The `@graphql-tools/stitching-directives` package provides importable directives definitions that can be used to annotate types and fields within subschemas, a validator to ensure the directives are used appropriately, and a configuration transformer that can be used on the gateway to convert the subschema directives into explicit configuration setting. + +It also provides pre-built directives to be used with code-first schemas that do not parse SDL. The validator is configured to read directives from GraphQL entity extensions, which actually take priority when present over the SDL. + +The `@graphql-tools/utils` package also exports a function that can print these "directives within extensions" as actual directives that can be then exposed via subservice to the gateway. + +Note: the service setup in this example is based on the [official demonstration repository](https://github.com/apollographql/federation-demo) for +[Apollo Federation](https://www.apollographql.com/docs/federation/). + +**This example demonstrates:** + +- Use of the @key, @computed and @merge "directives within extensions" to specify type merging configuration. + +## Setup + +```shell +cd code-first-schemas + +yarn install +yarn start-services +``` + +The following services are available for interactive queries: + +- **Stitched gateway:** http://localhost:4000/graphql +- _Accounts subservice_: http://localhost:4001/graphql +- _Inventory subservice_: http://localhost:4002/graphql +- _Products subservice_: http://localhost:4003/graphql +- _Reviews subservice_: http://localhost:4004/graphql + +## Summary + +First, try a query that includes data from all services: + +```graphql +query { + products(upcs: [1, 2]) { + name + price wit + weight + inStock + shippingEstimate + reviews { + id + body + author { + name + username + totalReviews + } + product { + name + price + } + } + } +} +``` + +Neat, it works! All those merges were configured through schema annotations within schemas! + +### Accounts subservice + +The Accounts subservice showcases how schemas created with vanilla `graphql-js` can also utilize stitching directives to achieve the benefits of colocating types and their merge configuration, including support for hot-reloading: + +- _Directive usages_: implemented as "directives within extensions," i.e. following the Gatsby/graphql-compose convention of embedding third party directives under the `directives` key of each GraphQL entity's `extensions` property. +- _Directive declarations_: directly added to the schema by using the compiled directives exported by the `@graphql-tools/stitching-directives` package. + +### Inventory subservice + +The Inventory subservice demonstrates using stitching directives with a schema created using the `nexus` library: + +- _Directive usages_: implemented as "directives within extensions," i.e. following the Gatsby/graphql-compose convention of embedding third party directives under the `directives` key of each GraphQL entity's `extensions` property. +- _Directive declarations_: `nexus` does not yet support passing in built `graph-js` `GraphQLDirective` objects, but you can easily create a new schema from the `nexus` schema programatically (using `new GraphQLSchema({ ...originalSchema.toConfig(), directives: [...originalSchema.getDirectives(), ...allStitchingDirectives] })`. + +### Products subservice + +The Products subservice shows how `TypeGraphQL` can easily implement third party directives including stitching directives. + +- _Directive usages_: implemented using the @Directive decorator syntax, TypeGraphQL's method of supporting third party directives within its code-first schema. +- _Directive declarations_: not strictly required -- TypeGraphQL does not validate the directive usage SDL, and creates actual directives under the hood, as if they were created with SDL, so directive declarations are actually not required. This makes setup a bit easier, at the cost of skipping a potentially helpful validation step. + +# Reviews subservice +The Reviews subservice is available for comparison to remind us of how `makeExecutableSchema` utilizes directives with SDL. + +- _Directive usages_: implemented using directives within actual SDL. +- _Directive declarations_: directive type definitions are imported from the `@graphql-tools/stitching-directives` package. diff --git a/code-first-schemas/index.js b/code-first-schemas/index.js new file mode 100644 index 0000000..d6265e1 --- /dev/null +++ b/code-first-schemas/index.js @@ -0,0 +1,69 @@ +const { stitchSchemas } = require('@graphql-tools/stitch'); +const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); +const { buildSchema } = require('graphql'); +const makeServer = require('./lib/make_server'); +const makeRemoteExecutor = require('./lib/make_remote_executor'); + +const { stitchingDirectivesTransformer } = stitchingDirectives(); + +async function makeGatewaySchema() { + const accountsExec = makeRemoteExecutor('http://localhost:4001/graphql'); + const inventoryExec = makeRemoteExecutor('http://localhost:4002/graphql'); + const productsExec = makeRemoteExecutor('http://localhost:4003/graphql'); + const reviewsExec = makeRemoteExecutor('http://localhost:4004/graphql'); + + return stitchSchemas({ + subschemaConfigTransforms: [stitchingDirectivesTransformer], + subschemas: [ + { + schema: await fetchRemoteSchema(accountsExec), + executor: accountsExec, + }, + { + schema: await fetchRemoteSchema(inventoryExec), + executor: inventoryExec, + }, + { + schema: await fetchRemoteSchema(productsExec), + executor: productsExec, + }, + { + schema: await fetchRemoteSchema(reviewsExec), + executor: reviewsExec, + } + ] + }); +} + +// fetch remote schemas with a retry loop +// (allows the gateway to wait for all services to startup) +async function fetchRemoteSchema(executor) { + return new Promise((resolve, reject) => { + async function next(attempt=1) { + try { + const { data } = await executor({ document: '{ _sdl }' }); + resolve(buildSchema(data._sdl)); + // Or: + // + // resolve(buildSchema(data._sdl, { assumeValidSDL: true })); + // + // `assumeValidSDL: true` is necessary if a code-first schema implements directive + // usage, either directly or by extensions, but not addition of actual custom + // directives. Alternatively, a new schema with the directives could be created + // from the nexus schema using: + // + // const newSchema = new GraphQLSchema({ + // ...originalSchema.toConfig(), + // directives: [...originalSchema.getDirectives(), ...allStitchingDirectives] + // }); + // + } catch (err) { + if (attempt >= 10) reject(err); + setTimeout(() => next(attempt+1), 300); + } + } + next(); + }); +} + +makeGatewaySchema().then(schema => makeServer(schema, 'gateway', 4000)); diff --git a/code-first-schemas/lib/make_remote_executor.js b/code-first-schemas/lib/make_remote_executor.js new file mode 100644 index 0000000..32ad3e8 --- /dev/null +++ b/code-first-schemas/lib/make_remote_executor.js @@ -0,0 +1,14 @@ +const { fetch } = require('cross-fetch'); +const { print } = require('graphql'); + +module.exports = function makeRemoteExecutor(url) { + return async ({ document, variables }) => { + const query = typeof document === 'string' ? document : print(document); + const fetchResult = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + return fetchResult.json(); + }; +}; diff --git a/code-first-schemas/lib/make_server.js b/code-first-schemas/lib/make_server.js new file mode 100644 index 0000000..32acb5c --- /dev/null +++ b/code-first-schemas/lib/make_server.js @@ -0,0 +1,8 @@ +const express = require('express'); +const { graphqlHTTP } = require('express-graphql'); + +module.exports = function makeServer(schema, name, port=4000) { + const app = express(); + app.use('/graphql', graphqlHTTP({ schema, graphiql: true })); + app.listen(port, () => console.log(`${name} running at http://localhost:${port}/graphql`)); +}; diff --git a/code-first-schemas/lib/not_found_error.js b/code-first-schemas/lib/not_found_error.js new file mode 100644 index 0000000..ffee03c --- /dev/null +++ b/code-first-schemas/lib/not_found_error.js @@ -0,0 +1,6 @@ +module.exports = class NotFoundError extends Error { + constructor(message) { + super(message || 'Record not found'); + this.extensions = { code: 'NOT_FOUND' }; + } +}; diff --git a/code-first-schemas/lib/read_file_sync.js b/code-first-schemas/lib/read_file_sync.js new file mode 100644 index 0000000..5748f02 --- /dev/null +++ b/code-first-schemas/lib/read_file_sync.js @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); + +module.exports = function readFileSync(dir, filename) { + return fs.readFileSync(path.join(dir, filename), 'utf8'); +}; diff --git a/code-first-schemas/package.json b/code-first-schemas/package.json new file mode 100644 index 0000000..bf5b914 --- /dev/null +++ b/code-first-schemas/package.json @@ -0,0 +1,33 @@ +{ + "name": "stitching-directives-sdl", + "version": "0.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start-service-accounts": "nodemon -e js,graphql services/accounts/index.js", + "start-service-inventory": "nodemon -e js,graphql services/inventory/index.js", + "start-service-products": "nodemon --watch services/products/**/*.ts --exec ts-node services/products/index.ts", + "start-service-reviews": "nodemon -e js,graphql services/reviews/index.js", + "start-service-gateway": "nodemon -e js,graphql index.js", + "start-services": "concurrently \"yarn:start-service-*\"" + }, + "dependencies": { + "@graphql-tools/schema": "^7.1.2", + "@graphql-tools/stitch": "^7.1.6", + "@graphql-tools/stitching-directives": "^1.1.0", + "@graphql-tools/utils": "^7.2.3", + "@types/node": "^14.14.16", + "class-validator": "^0.12.2", + "concurrently": "^5.3.0", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-graphql": "^0.12.0", + "graphql": "^15.4.0", + "nexus": "^1.0.0", + "nodemon": "^2.0.6", + "reflect-metadata": "^0.1.13", + "ts-node": "^9.1.1", + "type-graphql": "^1.1.1", + "typescript": "^4.1.3" + } +} diff --git a/code-first-schemas/services/accounts/index.js b/code-first-schemas/services/accounts/index.js new file mode 100644 index 0000000..9cf4d09 --- /dev/null +++ b/code-first-schemas/services/accounts/index.js @@ -0,0 +1,2 @@ +const makeServer = require('../../lib/make_server'); +makeServer(require('./schema'), 'accounts', 4001); diff --git a/code-first-schemas/services/accounts/schema.js b/code-first-schemas/services/accounts/schema.js new file mode 100644 index 0000000..932f79d --- /dev/null +++ b/code-first-schemas/services/accounts/schema.js @@ -0,0 +1,74 @@ +const { + GraphQLScalarType, + GraphQLSchema, + GraphQLObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLString, + GraphQLID, + specifiedDirectives +} = require('graphql'); +const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); +const { printSchemaWithDirectives } = require('@graphql-tools/utils'); +const NotFoundError = require('../../lib/not_found_error'); + +const { allStitchingDirectives, stitchingDirectivesValidator } = stitchingDirectives(); + +const users = [ + { id: '1', name: 'Ada Lovelace', username: '@ada' }, + { id: '2', name: 'Alan Turing', username: '@complete' }, +]; + +const accountsSchemaTypes = Object.create(null); + +accountsSchemaTypes._Key = new GraphQLScalarType({ + name: '_Key', +}); +accountsSchemaTypes.Query = new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + me: { + type: accountsSchemaTypes.User, + resolve: () => users[0], + }, + user: { + type: accountsSchemaTypes.User, + args: { + id: { + type: new GraphQLNonNull(GraphQLID), + }, + }, + resolve: (_root, { id }) => users.find(user => user.id === id) || new NotFoundError(), + extensions: { directives: { merge: { keyField: 'id' } } }, + }, + _sdl: { + type: new GraphQLNonNull(GraphQLString), + resolve(_root, _args, _context, info) { + return printSchemaWithDirectives(info.schema); + } + }, + }), +}); + +accountsSchemaTypes.User = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + id: { type: GraphQLID }, + name: { type: GraphQLString }, + username: { type: GraphQLString }, + }), + extensions: { + directives: { + key: { + selectionSet: '{ id }', + }, + }, + }, +}); + +const accountsSchema = new GraphQLSchema({ + query: accountsSchemaTypes.Query, + directives: [...specifiedDirectives, ...allStitchingDirectives], +}); + +module.exports = stitchingDirectivesValidator(accountsSchema); \ No newline at end of file diff --git a/code-first-schemas/services/inventory/index.js b/code-first-schemas/services/inventory/index.js new file mode 100644 index 0000000..45cc3ab --- /dev/null +++ b/code-first-schemas/services/inventory/index.js @@ -0,0 +1,2 @@ +const makeServer = require('../../lib/make_server'); +makeServer(require('./schema'), 'inventory', 4002); diff --git a/code-first-schemas/services/inventory/schema.js b/code-first-schemas/services/inventory/schema.js new file mode 100644 index 0000000..4e8bcf7 --- /dev/null +++ b/code-first-schemas/services/inventory/schema.js @@ -0,0 +1,97 @@ +const { scalarType, objectType, nonNull, list, queryType, makeSchema } = require('nexus'); +const { GraphQLSchema } = require('graphql'); +const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); +const { printSchemaWithDirectives } = require('@graphql-tools/utils'); +const NotFoundError = require('../../lib/not_found_error'); +const readFileSync = require('../../lib/read_file_sync'); + +const { allStitchingDirectives, stitchingDirectivesValidator } = stitchingDirectives(); + +const inventories = [ + { upc: '1', unitsInStock: 3 }, + { upc: '2', unitsInStock: 0 }, + { upc: '3', unitsInStock: 5 }, +]; + +const _Key = scalarType({ + name: '_Key', +}); + +const Product = objectType({ + name: 'Product', + definition(t) { + t.nonNull.id('upc'); + t.boolean('inStock', { + resolve(product) { + return product.unitsInStock > 0; + } + }); + t.int('shippingEstimate', { + resolve(product) { + // free for expensive items, otherwise estimate based on weight + return product.price > 1000 ? 0 : Math.round(product.weight * 0.5); + }, + extensions: { + directives: { + computed: { + selectionSet: '{ price weight }', + }, + }, + }, + }); + }, + extensions: { + directives: { + key: { + selectionSet: '{ upc }', + }, + }, + }, +}); + +const Query = queryType({ + definition(t) { + t.field('mostStockedProduct', { + type: Product, + resolve() { + return inventories.reduce((acc, i) => acc.unitsInStock >= i.unitsInStock ? acc : i, inventories[0]); + } + }); + t.field('_products', { + type: nonNull(list(Product)), + args: { + keys: nonNull(list(nonNull(_Key))), + }, + resolve(_root, { keys }) { + return keys.map(key => { + const inventory = inventories.find(i => i.upc === key.upc); + return inventory ? { ...key, ...inventory } : new NotFoundError(); + }); + }, + extensions: { + directives: { + merge: {}, + }, + }, + }); + t.nonNull.string('_sdl', { + resolve(_root, _args, _context, info) { + return printSchemaWithDirectives(info.schema); + } + }); + }, +}); + +const inventorySchema = makeSchema({ types: [Query] }); + +// Directive usage without definitions will throw an error on the gateway when it attempts to build +// a non-executable schema from the subschema's SDL. The below code will add the definitions: +const extendedSchema = new GraphQLSchema({ + ...inventorySchema.toConfig(), + directives: [...inventorySchema.getDirectives(), ...allStitchingDirectives], +}); + +// Alternatively, the schema could be built on the gateway using options { assumeValidSDL: true }, +// but this would skip a layer of validation. + +module.exports = stitchingDirectivesValidator(extendedSchema); \ No newline at end of file diff --git a/code-first-schemas/services/products/index.ts b/code-first-schemas/services/products/index.ts new file mode 100644 index 0000000..5ee9f44 --- /dev/null +++ b/code-first-schemas/services/products/index.ts @@ -0,0 +1,6 @@ +import * as makeServer from '../../lib/make_server'; +import schema from './schema'; + +schema.then(s => { + makeServer(s, 'products', 4003); +}); diff --git a/code-first-schemas/services/products/schema.ts b/code-first-schemas/services/products/schema.ts new file mode 100644 index 0000000..09d0b8e --- /dev/null +++ b/code-first-schemas/services/products/schema.ts @@ -0,0 +1,65 @@ +import "reflect-metadata"; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { stitchingDirectives } from '@graphql-tools/stitching-directives'; +import { GraphQLResolveInfo, specifiedDirectives } from 'graphql'; +import { Arg, buildSchema, Directive, /* Extensions, */ Field, ID, Info, Int, ObjectType, Query, Resolver } from 'type-graphql'; +import * as NotFoundError from '../../lib/not_found_error'; + +const { allStitchingDirectives, stitchingDirectivesValidator } = stitchingDirectives(); + +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 }, +]; + +@Directive(`@key(selectionSet: "{ upc }")`) +// Or: +// @Extensions({ directives: { key: { selectionSet : '{ upc }' } } }) +@ObjectType() +class Product { + @Field(type => ID) + upc: string; + + @Field() + name: string; + + @Field(type => Int) + price: number; + + @Field(type => Int) + weight: number; +} + +@Resolver() +class ProductResolver { + @Query(returns => [Product]) + topProducts( + @Arg("first", type => Int, { defaultValue: 2 }) first: number, + ) { + return products.slice(0, first); + } + + @Directive(`@merge(keyField: "upc")`) + // Or: @Extensions({ directives: { merge: { keyField : 'upc' } } }) + @Query(returns => [Product]) + products( + @Arg("upcs", type => [ID]) upcs: Array, + ) { + return upcs.map((upc) => products.find(product => product.upc === upc) || new NotFoundError()); + } + + @Query(returns => String) + _sdl( + @Info() info: GraphQLResolveInfo, + ) { + return printSchemaWithDirectives(info.schema); + } +} + +const productsSchema = buildSchema({ + resolvers: [ProductResolver], + directives: [...specifiedDirectives, ...allStitchingDirectives], +}); + +export default productsSchema; \ No newline at end of file diff --git a/code-first-schemas/services/reviews/index.js b/code-first-schemas/services/reviews/index.js new file mode 100644 index 0000000..9e1af4c --- /dev/null +++ b/code-first-schemas/services/reviews/index.js @@ -0,0 +1,2 @@ +const makeServer = require('../../lib/make_server'); +makeServer(require('./schema'), 'reviews', 4004); diff --git a/code-first-schemas/services/reviews/schema.graphql b/code-first-schemas/services/reviews/schema.graphql new file mode 100644 index 0000000..df70444 --- /dev/null +++ b/code-first-schemas/services/reviews/schema.graphql @@ -0,0 +1,36 @@ +type Review { + id: ID! + body: String + author: User + product: Product +} + +type User @key(selectionSet: "{ id }") { + id: ID! + totalReviews: Int! + reviews: [Review] +} + +type Product @key(selectionSet: "{ upc }") { + upc: ID! + reviews: [Review] +} + +input UserKey { + id: ID! +} + +input ProductKey { + upc: ID! +} + +input ProductInput { + keys: [ProductKey!]! +} + +type Query { + review(id: ID!): Review + _users(keys: [UserKey!]!): [User]! @merge + _products(input: ProductInput): [Product]! @merge(keyArg: "input.keys") + _sdl: String! +} diff --git a/code-first-schemas/services/reviews/schema.js b/code-first-schemas/services/reviews/schema.js new file mode 100644 index 0000000..6d44cd2 --- /dev/null +++ b/code-first-schemas/services/reviews/schema.js @@ -0,0 +1,42 @@ +const { makeExecutableSchema } = require('@graphql-tools/schema'); +const { stitchingDirectives } = require('@graphql-tools/stitching-directives'); +const NotFoundError = require('../../lib/not_found_error'); +const readFileSync = require('../../lib/read_file_sync'); + +const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); + +const typeDefs = ` + ${stitchingDirectivesTypeDefs} + ${readFileSync(__dirname, 'schema.graphql')} +`; + +const reviews = [ + { id: '1', authorId: '1', productUpc: '1', body: 'Love it!' }, + { id: '2', authorId: '1', productUpc: '2', body: 'Too expensive.' }, + { id: '3', authorId: '2', productUpc: '3', body: 'Could be better.' }, + { id: '4', authorId: '2', productUpc: '1', body: 'Prefer something else.' }, +]; + +module.exports = makeExecutableSchema({ + schemaTransforms: [stitchingDirectivesValidator], + typeDefs, + resolvers: { + Review: { + author: (review) => ({ id: review.authorId }), + product: (review) => ({ upc: review.productUpc }), + }, + User: { + reviews: (user) => reviews.filter(review => review.authorId === user.id), + totalReviews: (user) => reviews.filter(review => review.authorId === user.id).length, + }, + Product: { + reviews: (product) => reviews.filter(review => review.productUpc === product.upc), + }, + Query: { + review: (_root, { id }) => reviews.find(review => review.id === id) || new NotFoundError(), + _users: (_root, { keys }) => keys, + _products: (_root, { input }) => input.keys, + _sdl: () => typeDefs, + }, + } +}); diff --git a/code-first-schemas/tsconfig.json b/code-first-schemas/tsconfig.json new file mode 100644 index 0000000..b12f146 --- /dev/null +++ b/code-first-schemas/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "lib": ["es2018", "esnext.asynciterable"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + } +} \ No newline at end of file