├── example ├── react-app │ ├── src │ │ ├── App.css │ │ ├── graphql │ │ │ ├── queries.js │ │ │ ├── fragments.js │ │ │ ├── subscriptions.js │ │ │ ├── mutations.js │ │ │ └── apollo.js │ │ ├── index.js │ │ ├── App.js │ │ ├── pages │ │ │ ├── Home.js │ │ │ └── AddPost.js │ │ └── index.css │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── .env.sample │ ├── Dockerfile │ └── package.json ├── gateway-server │ ├── src │ │ ├── services │ │ │ ├── authors │ │ │ │ ├── data.js │ │ │ │ ├── typeDefs.js │ │ │ │ ├── index.js │ │ │ │ └── resolvers.js │ │ │ └── posts │ │ │ │ ├── index.js │ │ │ │ ├── typeDefs.js │ │ │ │ ├── data.js │ │ │ │ └── resolvers.js │ │ ├── redis │ │ │ └── index.js │ │ └── index.js │ ├── .env.sample │ ├── Dockerfile │ └── package.json ├── subscriptions-server │ ├── src │ │ ├── typeDefs.js │ │ ├── redis │ │ │ └── index.js │ │ ├── resolvers.js │ │ ├── datasources │ │ │ └── LIveBlogDataSource │ │ │ │ └── index.js │ │ └── index.js │ ├── .env.sample │ ├── package.json │ └── Dockerfile ├── .dockerignore ├── docker-compose.yml └── architecture.drawio.svg ├── src ├── index.ts ├── utils │ ├── subscriptions.ts │ └── schema.ts └── datasources │ └── GatewayDataSource │ └── index.ts ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /example/react-app/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 1rem 2rem; 3 | } 4 | -------------------------------------------------------------------------------- /example/react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/authors/data.js: -------------------------------------------------------------------------------- 1 | export const authors = [ 2 | { id: 1, name: "Alice" }, 3 | { id: 2, name: "Bob" } 4 | ]; 5 | -------------------------------------------------------------------------------- /example/react-app/.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_GATEWAY_API_URL=http://localhost:4000/graphql 2 | REACT_APP_SUBSCRIPTIONS_API_URL=ws://localhost:5000/graphql -------------------------------------------------------------------------------- /example/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollosolutions/federation-subscription-tools/HEAD/example/react-app/public/favicon.ico -------------------------------------------------------------------------------- /example/react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollosolutions/federation-subscription-tools/HEAD/example/react-app/public/logo192.png -------------------------------------------------------------------------------- /example/react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollosolutions/federation-subscription-tools/HEAD/example/react-app/public/logo512.png -------------------------------------------------------------------------------- /example/subscriptions-server/src/typeDefs.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const typeDefs = gql` 4 | type Subscription { 5 | postAdded: Post 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /example/react-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install --no-optional && npm cache clean --force 8 | 9 | COPY . . 10 | -------------------------------------------------------------------------------- /example/.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | .dockerignore 4 | docker-compose* 5 | **/Dockerfile* 6 | 7 | **/npm-debug.log* 8 | **/node_modules/ 9 | 10 | tsconfig.tsbuildinfo 11 | 12 | .git 13 | .gitingore 14 | 15 | LICENSE 16 | README.md -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { addGatewayDataSourceToSubscriptionContext } from "./utils/subscriptions"; 2 | export { GatewayDataSource } from "./datasources/GatewayDataSource"; 3 | export { getGatewayApolloConfig, makeSubscriptionSchema } from "./utils/schema"; 4 | -------------------------------------------------------------------------------- /src/utils/subscriptions.ts: -------------------------------------------------------------------------------- 1 | export function addGatewayDataSourceToSubscriptionContext( 2 | context, 3 | gatewayDataSource 4 | ) { 5 | gatewayDataSource.initialize({ context, cache: undefined }); 6 | return { dataSources: { gatewayApi: gatewayDataSource } }; 7 | } 8 | -------------------------------------------------------------------------------- /example/react-app/src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | import { PostFields } from "./fragments"; 4 | 5 | export const GetPosts = gql` 6 | query GetPosts { 7 | posts { 8 | ...PostFields 9 | } 10 | } 11 | ${PostFields} 12 | `; 13 | -------------------------------------------------------------------------------- /example/react-app/src/graphql/fragments.js: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const PostFields = gql` 4 | fragment PostFields on Post { 5 | author { 6 | id 7 | name 8 | } 9 | content 10 | id 11 | publishedAt 12 | title 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /example/react-app/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | import { PostFields } from "./fragments"; 4 | 5 | export const PostAdded = gql` 6 | subscription PostAdded { 7 | postAdded { 8 | ...PostFields 9 | } 10 | } 11 | ${PostFields} 12 | `; 13 | -------------------------------------------------------------------------------- /example/react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | import "./index.css"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/authors/typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export const typeDefs = gql` 4 | type Author @key(fields: "id") { 5 | id: ID! 6 | name: String! 7 | # nomDePlum: String 8 | } 9 | 10 | extend type Query { 11 | author(id: ID!): Author 12 | authors: [Author] 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /example/gateway-server/src/redis/index.js: -------------------------------------------------------------------------------- 1 | import { RedisPubSub } from "graphql-redis-subscriptions"; 2 | import Redis from "ioredis"; 3 | 4 | export const redis = new Redis( 5 | process.env.REDIS_PORT, 6 | process.env.REDIS_HOST_ADDRESS 7 | ); 8 | 9 | export const pubsub = new RedisPubSub({ 10 | publisher: redis, 11 | subscriber: redis 12 | }); 13 | -------------------------------------------------------------------------------- /example/subscriptions-server/src/redis/index.js: -------------------------------------------------------------------------------- 1 | import { RedisPubSub } from "graphql-redis-subscriptions"; 2 | import Redis from "ioredis"; 3 | 4 | export const redis = new Redis( 5 | process.env.REDIS_PORT, 6 | process.env.REDIS_HOST_ADDRESS 7 | ); 8 | 9 | export const pubsub = new RedisPubSub({ 10 | publisher: redis, 11 | subscriber: redis 12 | }); 13 | -------------------------------------------------------------------------------- /example/gateway-server/.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | APOLLO_KEY= 4 | APOLLO_GRAPH_REF= 5 | APOLLO_GRAPH_VARIANT=current 6 | 7 | GATEWAY_PORT=4000 8 | AUTHORS_SERVICE_PORT=4001 9 | AUTHORS_SERVICE_URL=http://localhost:4001 10 | POSTS_SERVICE_PORT=4002 11 | POSTS_SERVICE_URL=http://localhost:4002 12 | 13 | REDIS_HOST_ADDRESS=redis 14 | REDIS_PORT=6379 15 | -------------------------------------------------------------------------------- /example/react-app/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | import { PostFields } from "./fragments"; 4 | 5 | export const AddPost = gql` 6 | mutation AddPost($authorID: ID!, $content: String!, $title: String!) { 7 | addPost(authorID: $authorID, content: $content, title: $title) { 8 | ...PostFields 9 | } 10 | } 11 | ${PostFields} 12 | `; 13 | -------------------------------------------------------------------------------- /example/subscriptions-server/.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | APOLLO_KEY= 4 | APOLLO_GRAPH_REF= 5 | APOLLO_GRAPH_VARIANT=current 6 | GATEWAY_ENDPOINT=http://gateway_server:4000/graphql 7 | 8 | AUTHORS_SERVICE_URL=http://gateway_server:4001 9 | POSTS_SERVICE_URL=http://gateway_server:4002 10 | 11 | REDIS_HOST_ADDRESS=redis 12 | REDIS_PORT=6379 13 | 14 | SUBSCRIPTIONS_SERVICE_PORT=5000 15 | -------------------------------------------------------------------------------- /example/gateway-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.16-alpine 2 | 3 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 4 | ENV PATH=$PATH:/home/node/.npm-global/bin 5 | 6 | RUN mkdir -p /home/node/app/node_modules && \ 7 | chown -R node:node /home/node/app 8 | 9 | WORKDIR /home/node/app 10 | 11 | USER node 12 | 13 | COPY package*.json ./ 14 | 15 | RUN npm install --no-optional && npm cache clean --force 16 | 17 | COPY --chown=node:node . . 18 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/posts/index.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server"; 2 | import { buildSubgraphSchema } from "@apollo/subgraph"; 3 | 4 | import { resolvers } from "./resolvers"; 5 | import { typeDefs } from "./typeDefs"; 6 | 7 | const schema = buildSubgraphSchema([{ typeDefs, resolvers }]); 8 | 9 | const server = new ApolloServer({ schema }); 10 | 11 | server.listen(process.env.POSTS_SERVICE_PORT).then(({ url }) => { 12 | console.log(`🚀 Posts service ready at ${url}`); 13 | }); 14 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/authors/index.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server"; 2 | import { buildSubgraphSchema } from "@apollo/subgraph"; 3 | 4 | import { resolvers } from "./resolvers"; 5 | import { typeDefs } from "./typeDefs"; 6 | 7 | const schema = buildSubgraphSchema([{ typeDefs, resolvers }]); 8 | 9 | const server = new ApolloServer({ schema }); 10 | 11 | server.listen(process.env.AUTHORS_SERVICE_PORT).then(({ url }) => { 12 | console.log(`🚀 Authors service ready at ${url}`); 13 | }); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .vscode 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # output 8 | dist 9 | 10 | # TypeScript 11 | *.tsbuildinfo 12 | 13 | # client 14 | client/build 15 | 16 | # logs 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # environment 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | # misc 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | *[Tt]humbs.db 36 | *.Trashes 37 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/authors/resolvers.js: -------------------------------------------------------------------------------- 1 | import { authors } from "./data"; 2 | 3 | export const resolvers = { 4 | Author: { 5 | __resolveReference(reference, context, info) { 6 | return authors.find(author => author.id === parseInt(reference.id)); 7 | } 8 | }, 9 | 10 | Query: { 11 | author(parent, { id }, context, info) { 12 | return authors.find(author => author.id === parseInt(id)); 13 | }, 14 | authors(parent, args, context, info) { 15 | return authors; 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/posts/typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export const typeDefs = gql` 4 | type Post { 5 | id: ID! 6 | author: Author! 7 | content: String! 8 | publishedAt: String! 9 | title: String! 10 | } 11 | 12 | extend type Author @key(fields: "id") { 13 | id: ID! @external 14 | posts: [Post] 15 | } 16 | 17 | extend type Query { 18 | post(id: ID!): Post 19 | posts: [Post] 20 | } 21 | 22 | extend type Mutation { 23 | addPost(authorID: ID!, content: String, title: String): Post 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /example/react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/subscriptions-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-server", 3 | "version": "1.0.0", 4 | "description": "A demonstration subscriptions service for a federated data graph.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "server": "nodemon -r esm -r dotenv/config ./src/index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@apollo/gateway": "^0.46.0", 14 | "dotenv": "^8.2.0", 15 | "esm": "^3.2.25", 16 | "graphql-redis-subscriptions": "^2.3.1", 17 | "graphql-tag": "^2.11.0", 18 | "graphql-ws": "^5.6.2", 19 | "ioredis": "^4.19.4", 20 | "nodemon": "^2.0.7", 21 | "ws": "^7.4.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | "composite": true, 6 | "target": "esnext", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "removeComments": true, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "noImplicitReturns": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedParameters": false, 19 | "noUnusedLocals": false, 20 | "forceConsistentCasingInFileNames": true 21 | }, 22 | "include": ["./src/**/*.ts"], 23 | "exclude": ["node_modules", "**/example/*"] 24 | } 25 | -------------------------------------------------------------------------------- /example/react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from "@apollo/client"; 2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 3 | import React from "react"; 4 | 5 | import AddPost from "./pages/AddPost"; 6 | import client from "./graphql/apollo"; 7 | import Home from "./pages/Home"; 8 | 9 | import "./App.css"; 10 | 11 | function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /example/subscriptions-server/src/resolvers.js: -------------------------------------------------------------------------------- 1 | import { pubsub } from "./redis"; 2 | 3 | const POST_ADDED = "POST_ADDED"; 4 | 5 | export const resolvers = { 6 | Subscription: { 7 | postAdded: { 8 | // The client may request `Post` fields that are not resolvable from the 9 | // payload data that was included in `pubsub.publish()`, so we must 10 | // provide some mechanism to fetch those additional fields when requested 11 | resolve(payload, args, { dataSources: { gatewayApi } }, info) { 12 | return gatewayApi.fetchAndMergeNonPayloadPostData( 13 | payload.postAdded.id, 14 | payload, 15 | info 16 | ); 17 | }, 18 | subscribe(_, args) { 19 | // Subscribe to `POST_ADDED` in the shared Redis instance 20 | return pubsub.asyncIterator([POST_ADDED]); 21 | } 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/posts/data.js: -------------------------------------------------------------------------------- 1 | export const posts = [ 2 | { 3 | id: 1, 4 | authorID: 1, 5 | title: "The Big Event - What We Know So Far", 6 | content: 7 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel ex lacinia, hendrerit lectus ullamcorper, pretium augue. Morbi ornare eu felis quis feugiat. In porta augue a erat viverra, vitae tincidunt mi ultrices.", 8 | publishedAt: "2020-09-09T19:31:24.000Z" 9 | }, 10 | { 11 | id: 2, 12 | authorID: 2, 13 | title: "Breaking Update About What Happened", 14 | content: 15 | "Nunc eu fringilla ex, nec mattis ante. Donec maximus a purus id viverra. Curabitur nulla magna, aliquam vitae venenatis vel, feugiat sed nisl. Ut non varius est, ac faucibus nisl. Pellentesque iaculis orci nunc, dapibus lacinia ante pulvinar ut.", 16 | publishedAt: "2020-09-09T20:04:57.000Z" 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-subscription-tools", 3 | "version": "0.2.0", 4 | "description": "A set of demonstration utilities to facilitate GraphQL subscription usage alongside a federated data graph.", 5 | "main": "dist/index.js", 6 | "types": "dist/types.d.ts", 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "scripts": { 11 | "compile": "tsc --build tsconfig.json", 12 | "compile:clean": "tsc --build tsconfig.json --clean", 13 | "watch": "tsc --build tsconfig.json --watch" 14 | }, 15 | "dependencies": { 16 | "@apollo/client": "^3.5.8", 17 | "apollo-datasource": "^0.7.3", 18 | "apollo-server": "^3.6.2", 19 | "graphql": "^15.5.0", 20 | "graphql-parse-resolve-info": "4.12.0", 21 | "graphql-tools": "^8.2.0", 22 | "lodash": "^4.17.21", 23 | "node-fetch": "^2.6.1" 24 | }, 25 | "devDependencies": { 26 | "@types/lodash": "^4.14.178", 27 | "@types/node-fetch": "^2.6.1", 28 | "typescript": "^4.5.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | 3 | import { makeExecutableSchema } from "graphql-tools"; 4 | import { gql } from "graphql-tag"; 5 | import { DocumentNode, printSchema } from "graphql"; 6 | 7 | export function getGatewayApolloConfig(key: string, graphRef: string) { 8 | return { 9 | key, 10 | graphRef, 11 | keyHash: createHash("sha512").update(key).digest("hex") 12 | }; 13 | } 14 | 15 | export function makeSubscriptionSchema({ 16 | gatewaySchema, 17 | typeDefs, 18 | resolvers 19 | }: any) { 20 | if (!typeDefs || !resolvers) { 21 | throw new Error( 22 | "Both `typeDefs` and `resolvers` are required to make the executable subscriptions schema." 23 | ); 24 | } 25 | 26 | const gatewayTypeDefs = gatewaySchema 27 | ? gql(printSchema(gatewaySchema)) 28 | : undefined; 29 | 30 | return makeExecutableSchema({ 31 | typeDefs: [ 32 | ...((gatewayTypeDefs && [gatewayTypeDefs]) as DocumentNode[]), 33 | typeDefs 34 | ], 35 | resolvers 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /example/subscriptions-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.16-alpine 2 | 3 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 4 | ENV PATH=$PATH:/home/node/.npm-global/bin 5 | 6 | RUN mkdir -p /home/node/app/node_modules \ 7 | /home/node/federation-subscription-tools/{dist,node_modules} && \ 8 | chown -R node:node /home/node/app && \ 9 | chown -R node:node /home/node/federation-subscription-tools 10 | 11 | USER node 12 | 13 | WORKDIR /home/node/federation-subscription-tools 14 | 15 | COPY package*.json tsconfig.json ./ 16 | 17 | RUN npm install -g graphql@15.5.0 && npm install --no-optional && \ 18 | npm cache clean --force && npm link && \ 19 | npm link graphql 20 | 21 | COPY --chown=node:node . . 22 | 23 | RUN npm run compile 24 | 25 | WORKDIR /home/node/app 26 | 27 | COPY example/subscriptions-server/package*.json ./ 28 | 29 | RUN npm install --no-optional && npm cache clean --force && \ 30 | npm link federation-subscription-tools && npm link graphql 31 | 32 | COPY --chown=node:node example/subscriptions-server . 33 | -------------------------------------------------------------------------------- /example/gateway-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "A demonstration federated data graph where the subgraphs publish subscription events to Redis pub/sub.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "server": "concurrently -k npm:server:*", 8 | "server:authors": "nodemon -r esm -r dotenv/config ./src/services/authors/index.js", 9 | "server:posts": "nodemon -r esm -r dotenv/config ./src/services/posts/index.js", 10 | "server:gateway": "wait-on tcp:4001 tcp:4002 && nodemon -r esm -r dotenv/config ./src/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@apollo/gateway": "^0.46.0", 17 | "@apollo/subgraph": "^0.3.1", 18 | "apollo-server": "^3.6.3", 19 | "concurrently": "^5.3.0", 20 | "dotenv": "^8.2.0", 21 | "esm": "^3.2.25", 22 | "graphql": "^15.5.0", 23 | "graphql-redis-subscriptions": "^2.3.1", 24 | "ioredis": "^4.19.4", 25 | "nodemon": "^2.0.7", 26 | "wait-on": "^5.2.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.5.10", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "graphql": "^15.5.0", 11 | "graphql-ws": "^5.6.2", 12 | "moment": "^2.27.0", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-router-dom": "^5.2.0", 16 | "react-scripts": "3.4.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/gateway-server/src/services/posts/resolvers.js: -------------------------------------------------------------------------------- 1 | import { pubsub } from "../../redis"; 2 | import { posts } from "./data"; 3 | 4 | const POST_ADDED = "POST_ADDED"; 5 | 6 | export const resolvers = { 7 | Author: { 8 | posts(author, args, context, info) { 9 | return posts.filter(post => post.authorId === author.id); 10 | } 11 | }, 12 | 13 | Post: { 14 | author(post) { 15 | return { __typename: "Author", id: post.authorID }; 16 | } 17 | }, 18 | 19 | Query: { 20 | post(root, { id }, context, info) { 21 | return posts.find(post => post.id === parseInt(id)); 22 | }, 23 | posts(root, args, context, info) { 24 | return posts; 25 | } 26 | }, 27 | 28 | Mutation: { 29 | addPost(root, args, context, info) { 30 | const postID = posts.length + 1; 31 | const post = { 32 | ...args, 33 | id: postID, 34 | publishedAt: new Date().toISOString() 35 | }; 36 | 37 | // Publish to `POST_ADDED` in the shared Redis instance 38 | pubsub.publish(POST_ADDED, { postAdded: post }); 39 | posts.push(post); 40 | return post; 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /example/react-app/src/graphql/apollo.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, HttpLink, InMemoryCache, split } from "@apollo/client"; 2 | import { createClient } from "graphql-ws"; 3 | import { getMainDefinition } from "@apollo/client/utilities"; 4 | import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; 5 | 6 | const wsLink = new GraphQLWsLink( 7 | createClient({ 8 | url: process.env.REACT_APP_SUBSCRIPTIONS_API_URL, 9 | connectionParams: () => { 10 | // simulate an auth token sent from the client over the WS connection 11 | const token = "some-token"; 12 | return { ...(token && { token }) }; 13 | } 14 | }) 15 | ); 16 | 17 | const httpLink = new HttpLink({ 18 | uri: process.env.REACT_APP_GATEWAY_API_URL 19 | }); 20 | 21 | const link = split( 22 | ({ query }) => { 23 | const definition = getMainDefinition(query); 24 | return ( 25 | definition.kind === "OperationDefinition" && 26 | definition.operation === "subscription" 27 | ); 28 | }, 29 | wsLink, 30 | httpLink 31 | ); 32 | 33 | const client = new ApolloClient({ 34 | cache: new InMemoryCache(), 35 | link 36 | }); 37 | 38 | export default client; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/gateway-server/src/index.js: -------------------------------------------------------------------------------- 1 | import { ApolloGateway } from "@apollo/gateway"; 2 | import { ApolloServer } from "apollo-server"; 3 | import { 4 | ApolloServerPluginUsageReporting, 5 | ApolloServerPluginUsageReportingDisabled 6 | } from "apollo-server-core"; 7 | 8 | const isProd = process.env.NODE_ENV === "production"; 9 | const apolloKey = process.env.APOLLO_KEY; 10 | 11 | let gatewayOptions = { 12 | debug: isProd ? false : true 13 | }; 14 | 15 | if (!apolloKey) { 16 | console.log("Head to https://studio.apollographql.com an create an account"); 17 | 18 | gatewayOptions.serviceList = [ 19 | { name: "authors", url: process.env.AUTHORS_SERVICE_URL }, 20 | { name: "posts", url: process.env.POSTS_SERVICE_URL } 21 | ]; 22 | } 23 | 24 | const apolloUsageReportingPlugin = apolloKey 25 | ? ApolloServerPluginUsageReporting() 26 | : ApolloServerPluginUsageReportingDisabled(); 27 | 28 | const gateway = new ApolloGateway(gatewayOptions); 29 | const server = new ApolloServer({ 30 | gateway, 31 | subscriptions: false, 32 | plugins: [apolloUsageReportingPlugin] 33 | }); 34 | 35 | server.listen(process.env.GATEWAY_PORT).then(({ url }) => { 36 | console.log(`🚀 Gateway API running at ${url}`); 37 | }); 38 | -------------------------------------------------------------------------------- /example/subscriptions-server/src/datasources/LIveBlogDataSource/index.js: -------------------------------------------------------------------------------- 1 | import { GatewayDataSource } from "federation-subscription-tools"; 2 | import gql from "graphql-tag"; 3 | 4 | export class LiveBlogDataSource extends GatewayDataSource { 5 | constructor(gatewayUrl) { 6 | super(gatewayUrl); 7 | } 8 | 9 | willSendRequest(request) { 10 | if (!request.headers) { 11 | request.headers = {}; 12 | } 13 | 14 | request.headers["apollographql-client-name"] = "Subscriptions Service"; 15 | request.headers["apollographql-client-version"] = "0.1.0"; 16 | 17 | // Forwards the encoded token extracted from the `connectionParams` with 18 | // the request to the gateway 19 | request.headers.authorization = `Bearer ${this.context.token}`; 20 | } 21 | 22 | async fetchAndMergeNonPayloadPostData(postID, payload, info) { 23 | const selections = this.buildNonPayloadSelections(payload, info); 24 | const payloadData = Object.values(payload)[0]; 25 | 26 | if (!selections) { 27 | return payloadData; 28 | } 29 | 30 | const Subscription_GetPost = gql` 31 | query Subscription_GetPost($id: ID!) { 32 | post(id: $id) { 33 | ${selections} 34 | } 35 | } 36 | `; 37 | 38 | try { 39 | const response = await this.query(Subscription_GetPost, { 40 | variables: { id: postID } 41 | }); 42 | return this.mergeFieldData(payloadData, response.data.post); 43 | } catch (error) { 44 | console.error(error); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | redis: 5 | image: redis:5.0.9-alpine 6 | container_name: redis 7 | restart: always 8 | ports: 9 | - 6379:6379 10 | gateway_server: 11 | container_name: gateway_server 12 | restart: always 13 | build: 14 | context: ./gateway-server 15 | ports: 16 | - 4000:4000 17 | - 4001:4001 18 | - 4002:4002 19 | volumes: 20 | - ./gateway-server:/home/node/app 21 | - /home/node/app/node_modules 22 | depends_on: 23 | - redis 24 | env_file: 25 | - ./gateway-server/.env 26 | command: npm run server 27 | subscriptions_server: 28 | container_name: subscriptions_server 29 | restart: always 30 | build: 31 | context: ../ 32 | dockerfile: example/subscriptions-server/Dockerfile 33 | ports: 34 | - 5000:5000 35 | volumes: 36 | - ../example/subscriptions-server:/home/node/app 37 | - ../:/home/node/federation-subscription-tools 38 | - /home/node/app/node_modules 39 | - /home/node/federation-subscription-tools/dist 40 | - /home/node/federation-subscription-tools/node_modules 41 | depends_on: 42 | - gateway_server 43 | - redis 44 | env_file: 45 | - ./subscriptions-server/.env 46 | command: npm run server 47 | react_app: 48 | container_name: react_app 49 | restart: always 50 | build: 51 | context: ./react-app 52 | volumes: 53 | - ./react-app:/usr/src/app 54 | - /usr/src/app/node_modules 55 | env_file: 56 | - ./react-app/.env 57 | ports: 58 | - 3000:3000 59 | stdin_open: true 60 | command: npm start 61 | -------------------------------------------------------------------------------- /example/react-app/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useQuery } from "@apollo/client"; 3 | import moment from "moment"; 4 | import React, { useEffect } from "react"; 5 | 6 | import { GetPosts } from "../graphql/queries"; 7 | import { PostAdded } from "../graphql/subscriptions"; 8 | 9 | function Home() { 10 | const { data, loading, subscribeToMore } = useQuery(GetPosts); 11 | 12 | useEffect(() => { 13 | const unsubscribe = subscribeToMore({ 14 | document: PostAdded, 15 | updateQuery: (prev, { subscriptionData }) => { 16 | if (!subscriptionData.data) { 17 | return prev; 18 | } 19 | return { 20 | posts: [...prev.posts, subscriptionData.data.postAdded] 21 | }; 22 | } 23 | }); 24 | return () => unsubscribe(); 25 | }, [subscribeToMore]); 26 | 27 | if (loading) { 28 | return

Loading...

; 29 | } 30 | 31 | return ( 32 |
33 | 38 | {data?.posts?.length ? ( 39 | [...data.posts] 40 | .filter(post => post !== null) 41 | .sort((a, b) => 42 | Date.parse(b.publishedAt) > Date.parse(a.publishedAt) ? 1 : -1 43 | ) 44 | .map(({ author, content, id, title, publishedAt }) => ( 45 |
46 |

{title}

47 |

Post ID: {id}

48 |

By {author.name}

49 |

{moment(publishedAt).format("h:mm A MMM D, YYYY")}

50 |

{content}

51 |
52 | )) 53 | ) : ( 54 |

No posts available!

55 | )} 56 |
57 | ); 58 | } 59 | 60 | export default Home; 61 | -------------------------------------------------------------------------------- /example/react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Real-time Federation Demo 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/react-app/src/pages/AddPost.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { useMutation } from "@apollo/client"; 3 | import React, { useState } from "react"; 4 | 5 | import { AddPost as AddPostMutation } from "../graphql/mutations"; 6 | 7 | function AddPost() { 8 | const [content, setContent] = useState(""); 9 | const [title, setTitle] = useState(""); 10 | const [completedMessage, setCompletedMessage] = useState(""); 11 | 12 | const [addPost] = useMutation(AddPostMutation, { 13 | onCompleted() { 14 | setContent(""); 15 | setTitle(""); 16 | setCompletedMessage("Your post was published!"); 17 | } 18 | }); 19 | 20 | return ( 21 |
22 | 27 |

Add a New Post

28 |
{ 30 | event.preventDefault(); 31 | setCompletedMessage(""); 32 | addPost({ variables: { authorID: 1, content, title } }); 33 | }} 34 | > 35 |
36 | 46 |
47 |
48 |