GraphQL subscription isn't triggered after PubSub publish - websocket

I'm trying to create a basic subscriptions system using WebSockets, TypeGraphQL and Apollo Server. I've created the WS server and it seems to work correctly.
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import { ApolloServer } from 'apollo-server-express'
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
import { buildSchema } from 'type-graphql'
import { MessageResolver } from './resolvers/messageResolver'
import { appDataSource } from './dataSource'
import { PubSub } from 'graphql-subscriptions'
import http from 'http'
import { WebSocketServer } from 'ws'
const main = async () => {
appDataSource.initialize().then(() => {
console.log("DataSource initialized.")
})
const app = express()
app.set('trust proxy', 1)
app.use(
cors({
origin: process.env.CORS_ORIGIN,
credentials: true,
})
)
app.use(cookieParser())
const schema = await buildSchema({
resolvers: [MessageResolver],
validate: false
})
const pubsub = new PubSub();
const apolloServer = new ApolloServer({
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground(),
],
schema,
context: ({ req, res }) => ({ req, res, pubsub }),
})
await apolloServer.start()
apolloServer.applyMiddleware({ app, cors: false })
const httpServer = http.createServer()
const wss = new WebSocketServer({
server: httpServer
})
httpServer.on('request', app);
httpServer.listen(5000, function () {
console.log(`http/ws server listening on 5000`);
});
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log(`received: ${message}`);
ws.send(JSON.stringify({
answer: 42
}));
});
});
}
main().catch((err) => {
console.error(err.stack);
})
Here are my subscription and my mutation:
#Subscription({
topics: "NOTIFICATIONS"
})
test(
#Root() message: string,
): string {
return message
}
#Mutation(() => Boolean)
async addNewComment(
#Arg("message") message: string,
#PubSub("NOTIFICATIONS") publish: Publisher<string>
) {
await Message.create({ message }).save()
await publish(message);
return true;
}
The problem I have is that when I use the mutation, the subscription should be triggered. This thing doesn't happen and I don't know why. Do you have any ideas?
Thank you!

Related

Apollo GraphQL react client: subscription

I was trying to make a little demo with GraphQL subscriptions and GraphQL Apollo client.
I already have my GraphQL API, but when I try to use Apollo client, it looks like it doesn't complete the websocket subscribe step:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '#apollo/client';
import { split, HttpLink } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { useSubscription } from '#apollo/react-hooks'
import reportWebVitals from './reportWebVitals';
const httpLink = new HttpLink({
uri: 'https://mygraphql.api'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://mygraphql.api',
options: {
reconnect: true
}
}));
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
fetchOptions: {
mode: 'no-cors',
}
});
const FAMILIES_SUBSCRIPTION = gql`
subscription{
onFamilyCreated {
id
name
}
}
`;
function LastFamily() {
const { loading, error, data } = useSubscription(FAMILIES_SUBSCRIPTION, {
variables: { },
onData: data => console.log('new data', data)
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
console.log(data);
const family = data.onFamilyCreated[0];
return (
<div>
<h1>{family.name}</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
(<ApolloProvider client={client}>
<div>
<LastFamily />
</div>
</ApolloProvider>));
reportWebVitals();
According to graphql-transport-ws, to accomplish a success call, it should call connection_init and subscribe message. But when I open Dev Tools, it only sends "connection_init"
I'm expecting this output:
What step should I add to accomplish a successful call using graphql-transport-ws?
P.s. I'm not a React Developer, just be kind.
The solutions I'm putting up are based on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of useSubscription hook. In light of my observations, I believe there may be some issues with your backend code that require refactoring. The transport library graphql-transport-ws has been deprecated and advise to use graphql-ws. The following setup also applies as of 12.2022.
Subscription on the backend
Install the following dependencies.
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
Set up the db models, I will refer to mongodb using mongoose and it might look like this one e.g.
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const model = mongoose.model
const FamilySchema = new Schema({
name: {
type: String,
unique: true, //optional
trim: true,
}
})
FamilySchema.virtual('id').get(function () {
return this._id.toHexString()
})
FamilySchema.set('toJSON', {
virtuals: true,
transform: (document, retObj) => {
delete retObj.__v
},
})
const FamilyModel = model('FamilyModel', FamilySchema)
export default FamilyModel
Setup schema types & resolvers; it might look like this one e.g.
// typeDefs.js
const typeDefs = `#graphql
type Family {
id: ID!
name: String!
}
type Query {
families: [Family]!
family(familyId: ID!): Family!
}
type Mutation {
createFamily(name: String): Family
}
type Subscription {
familyCreated: Family
}
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import FamilyModel from '../models/Family.js'
const pubsub = new PubSub()
const Family = FamilyModel
const resolvers = {
Query: {
families: async () => {
try {
const families = await Family.find({})
return families
} catch (error) {
console.error(error.message)
}
},
family: async (parent, args) => {
const family = await Family.findById(args.familyId)
return family
},
Mutation: {
createFamily: async (_, args) => {
const family = new Family({ ...args })
try {
const savedFamily = await family.save()
const createdFamily = {
id: savedFamily.id,
name: savedFamily.name
}
// resolvers for backend family subscription with object iterator FAMILY_ADDED
pubsub.publish('FAMILY_CREATED', { familyCreated: createdFamily })
return family
} catch (error) {
console.error(error.message)
}
}
},
Subscription: {
familyCreated: {
subscribe: () => pubsub.asyncIterator('FAMILY_CREATED'),
}
},
Family: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
name: async (parent) => {
return parent.name
}
}
}
export default resolvers
At the main entry server file (e.g. index.js) the code might look like this one e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import typeDefs from './schema/tpeDefs.js'
import resolvers from './schema/resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
...
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server)
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
Subscription on the CRA frontend
Install the following dependencies.
$ npm i #apollo/client graphql graphql-ws
General connection setup e.g.
// src/client.js
import { ApolloClient, HttpLink, InMemoryCache, split } from '#apollo/client'
import { getMainDefinition } from '#apollo/client/utilities'
import { defaultOptions } from './graphql/defaultOptions'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
...
const baseUri = process.env.REACT_APP_BASE_URI // for the client
const wsBaseUri = process.env.REACT_APP_WS_BASE_URI // for the backend as websocket
const httpLink = new HttpLink({
uri: baseUri,
})
const wsLink = new GraphQLWsLink(
createClient({
url: wsBaseUri
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
export default client
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import client from './client'
import { ApolloProvider } from '#apollo/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
)
Define the operations types for the client: queries, mutation & subscription e.g.
// src/graphql.js
import { gql } from '#apollo/client'
// Queries
export const FAMILIES = gql`
query Families {
families {
id
name
}
}
`
export const FAMILY = gql`
query Family($familyId: ID) {
family {
id
name
}
}
`
// Mutation
export const CREATE_FAMILY = gql`
mutation createFamily($name: String!) {
createFamily(name: $name) {
id
name
}
}
`
// Subscription
export const FAMILY_SUBSCRIPTION = gql`
subscription {
familyCreated {
id
name
}
}
Components, it might look like this one e.g.
Apollo's useQuery hook provides us with access to a function called subscribeToMore. This function can be destructured and used to act on new data that comes in via subscription. This has the result of rendering our app real-time.
The subscribeToMore function utilizes a single object as an argument. This object requires configuration to listen for and respond to subscriptions.
At the very least, we must pass a subscription document to the document key in this object. This is a GraphQL document in which we define our subscription.
We can a updateQuery field that can be used to update the cache, similar to how we would do in a mutation.
// src/components/CreateFamilyForm.js
import { useMutation } from '#apollo/client'
import { CREATE_FAMILY, FAMILIES } from '../graphql'
...
const [createFamily, { error, loading, data }] = useMutation(CREATE_FAMILY, {
refetchQueries: [{ query: FAMILIES }], // be sure to refetchQueries after mutation
})
...
// src/components/FamilyList.js
import React, { useEffect, useState } from 'react'
import { useQuery } from '#apollo/client'
import { Families, FAMILY_SUBSCRIPTION } from '../graphql'
const { cloneDeep, orderBy } = pkg
...
export const FamilyList = () => {
const [families, setFamilies] = useState([])
const { loading, error, data, subscribeToMore } = useQuery(Families)
...
useEffect(() => {
if (data?.families) {
setFamilies(cloneDeep(data?.families)) // if you're using lodash but it can be also setFamilies(data?.families)
}
}, [data?.families])
useEffect(() => {
subscribeToMore({
document: FAMILY_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newFamily = subscriptionData.data.familyCreated
if (!prev.families.find((family) => family.id === newFamily.id)) {
return Object.assign({}, prev.families, {
families: [...prev.families, newFamily],
})
} else {
return prev
}
},
})
}, [subscribeToMore])
const sorted = orderBy(families, ['names'], ['desc']) // optional; order/sort the list
...
console.log(sorted)
// map the sorted on the return statement
return(...)
END. Hard-coding some of the default resolvers are useful for ensuring that the value that you expect will returned while avoiding the return of null values. Perhaps not in every case, but for fields that refer to other models or schema.
Happy coding!

Setting up graphql yoga with websockets in nextjs api

In graphql yoga documentation, I found this example for using graphql yoga with websockets but it's in nodejs environment. How can I setup a server in nextjs api using this example? All advice is appreciated, thanks.
import { createServer } from '#graphql-yoga/node'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
async function main() {
const yogaApp = createServer({
graphiql: {
// Use WebSockets in GraphiQL
subscriptionsProtocol: 'WS'
}
})
// Get NodeJS Server from Yoga
const httpServer = await yogaApp.start()
// Create WebSocket server instance from our Node server
const wsServer = new WebSocketServer({
server: httpServer,
path: yogaApp.getAddressInfo().endpoint
})
// Integrate Yoga's Envelop instance and NodeJS server with graphql-ws
useServer(
{
execute: (args: any) => args.rootValue.execute(args),
subscribe: (args: any) => args.rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } =
yogaApp.getEnveloped(ctx)
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
rootValue: {
execute,
subscribe
}
}
const errors = validate(args.schema, args.document)
if (errors.length) return errors
return args
}
},
wsServer
)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

Apollo studio CORS error unable to reach server

I'm trying to setup apollo server in my NextJS project, I'm using apollo-server-micro and I ran into these issues:
the apollo studio sandbox is unable to reach the server due to CORS.
This is in pages/api/graphql
import { ApolloServer } from "apollo-server-micro";
import { typeDefs } from "./schemas";
import { resolvers } from "./resolvers/index";
import { createContext } from "./db/context";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: ({ res, req }) => createContext(res, req),
uploads: false,
introspection: true,
formatError: (error) => {
console.log(error);
return error;
},
});
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
module.exports = apolloServer
.start()
.then(() => apolloServer.createHandler({ path: "/api/graphql" }));
then I tried to enable cors but without success because I get this error:
API resolved without sending a response for /api/graphql, this may result in stalled requests.
import { ApolloServer } from "apollo-server-micro";
import { typeDefs } from "./schemas";
import { resolvers } from "./resolvers/index";
import { createContext } from "./db/context";
import { send } from "micro";
import cors from "micro-cors";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: ({ res, req }) => createContext(res, req),
uploads: false,
introspection: true,
formatError: (error) => {
console.log(error);
return error;
},
});
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
module.exports = apolloServer.start().then(() => {
const handler = apolloServer.createHandler({ path: "/api/graphql" });
return cors((req, res) => {
console.log(res);
return req.method === "OPTIONS" ? send(res, 200, 'ok') : handler(req, res);
});
});

Using passport-google-oauth2 authentication in server - client app

I have a TypeOrm/React app connected with mongodb database, where server is on port 4000, and client is on port 3000.
On client I created file googleAuth:
import passport from 'passport';
import {User} from './entity/User';
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const googleOptions = {
clientID: process.env.clientID,
clientSecret: process.env.clientSecret,
callbackURL: 'http://localhost:4000/auth/google/callback',
profileFields: ['id', 'email', 'first_name', 'last_name'],
};
const googleCallback = (_accessToken: any, _refreshToken: any, profile: any, done: any) => {
const matchingUser = User.findOne({googleId: profile.id});
if (matchingUser) {
done(null, matchingUser);
return;
}
const newUser = {
googleId: profile.id,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
email: profile.emails && profile.emails[0] && profile.emails[0].value
};
User.create(newUser);
done(null, newUser);
}
export const googleAuth = () => passport.use(new GoogleStrategy(
googleOptions,
googleCallback
));
My index.ts(server) file looks like this:
import "reflect-metadata";
import {createConnection} from "typeorm";
import "dotenv/config"
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import cors from "cors";
import { PostResolver } from "./resolvers/PostResolver";
import { UserResolver } from "./resolvers/UserResolver";
import path from 'path';
import passport from "passport";
import { googleAuth} from './googleAuth';
(async () => {
await createConnection();
googleAuth();
passport.serializeUser(function(user: any, done: any) {
done(null, user);
});
passport.deserializeUser(function(user: any, done: any) {
done(null, user);
});
const app = express();
app.use(cors());
app.use(passport.initialize());
app.get('/auth/google', passport.authenticate('google', { scope: ['email'] }));
app.get('/auth/google/callback', passport.authenticate('google', {
successRedirect: 'http://localhost:4000/graphql',
failureRedirect: 'http://localhost:4000/graphql',
}));
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [ PostResolver, UserResolver ]
}),
context: ({ req }) => ({
getUser: () => req.user,
logout: () => req.logout()
})
// context: ({ req, res }) => ({ req, res })
});
apolloServer.applyMiddleware({ app });
app.use(express.static('public'));
app.get('*', (_, res) => {
res.sendFile(path.resolve(__dirname, '../public', 'index.html'))
});
const port = process.env.PORT || 4000;
app.listen(port, () => console.log(`Server started on port ${port}`));
})();
My UserResolver:
import {
Resolver,
Query,
Ctx
} from "type-graphql";
import { User } from "../entity/User";
import { MyContext } from '../MyContext';
import { ExpressContext } from "apollo-server-express/dist/ApolloServer";
#Resolver()
export class UserResolver {
#Query(() => [User])
users() {
return User.find();
}
#Query(() => String)
getUser(#Ctx() { req }: ExpressContext) {
console.log(req);
return `your user id is: ${req.user}`;
}
}
In https://console.developers.google.com in credenatials I add
Authorised JavaScript origins: http://localhost:4000
In Authorised redirect URIs: http://localhost:4000/auth/google/callback
I can log in, but I can't get any data. I know that it's very messy code, but I need to organize this and rewrite it.
First, I want to know what domains and redirect should I add in my credentials.
Second is to configure server.
Third to create UserResolvers.
Fourth to get data with client.

Access to Apollo server for NestJS GraphQL test

A standard way to test an Apollo GraphQL server is to use the Apollo test client.
The createTestClient method requires a server argument.
In a NestJS/TypeGraphQL application, what's the appropriate way to access the Apollo server that's created by GraphQLModule from inside a (Jest) test?
const moduleFixture = await Test.createTestingModule({
imports: [ApplicationModule],
}).compile()
const app = await moduleFixture.createNestApplication(new ExpressAdapter(express)).init()
const module: GraphQLModule = moduleFixture.get<GraphQLModule>(GraphQLModule)
const apolloClient = createTestClient((module as any).apolloServer)
this is what i do
This code worked for me. Thanks to JustMe
import { Test, TestingModule } from '#nestjs/testing';
import { INestApplication } from '#nestjs/common';
import { createTestClient, TestQuery } from 'apollo-server-integration-testing';
import { AppModule } from './../src/app.module';
import { GraphQLModule } from '#nestjs/graphql';
describe('AppController (e2e)', () => {
let app: INestApplication;
// let mutateTest: TestQuery;
let correctQueryTest: TestQuery;
let wrongQueryTest: TestQuery;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
const module: GraphQLModule =
moduleFixture.get<GraphQLModule>(GraphQLModule);
const { query: correctQuery } = createTestClient({
apolloServer: (module as any).apolloServer,
extendMockRequest: {
headers: {
token:
'iIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWFiNmY0MjQ3YjEyYWNiNzQyYmQwYmYiLCJyb2xlIjoibWFuYWdlciIsImVtYWlsIjoibGVuYUBtYWlsLmNvbSIsInBhc3N3b3JkIjoiZTEwYWRjMzk0OWJhNTlhYmJlNTZlMDU3ZjIwZjg4M2UiLCJ1c2VybmFtZSI6ImxlbmEgZG9lIiwiY3JlYXRlZEF0IjoiMjAyMS0xMi0wNFQxMzozODoxMC4xMzZaIiwidXBkYXRlZEF0IjoiMjAyMS0xMi0wNFQxMzozODoxMC4xMzZaIiwiX192IjowLCJpYXQiOjE2Mzg2NTE4MjMsImV4cCI6MTYzODY1MTg4M30.d6SCh4x6Wwpj16UWf4ca-PbFCo1FQm_bLelp8kscG8U',
},
},
});
const { query: wrongQuery } = createTestClient({
apolloServer: (module as any).apolloServer,
});
// mutateTest = mutate;
correctQueryTest = correctQuery;
wrongQueryTest = wrongQuery;
});
it('/ Correct', async () => {
const result = await correctQueryTest(`
query FILTER_JOBS{
filterJobs(status: DONE) {
title,
status,
description,
lat,
long,
employees,
images,
assignedBy {
username,
email
}
}
}
`);
console.log(result);
});
it('/ Wrong', async () => {
const result = await wrongQueryTest(`
query FILTER_JOBS{
filterJobs(status: DONE) {
title,
status,
description,
lat,
long,
employees,
images,
assignedBy {
username,
email
}
}
}
`);
console.log(result);
});
});
After searching I ended using this:
import { getApolloServer } from '#nestjs/apollo';
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { ApolloServerBase, gql } from 'apollo-server-core';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
let apolloServer: ApolloServerBase<any>;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
apolloServer = getApolloServer(app);
});
afterAll(async () => {
await app.close();
});
it('signUp', async () => {
const signUpInput = gql`
mutation Mutation($signUpInput: SignUpInput!) {
signup(signUpInput: $signUpInput) {
access
refresh
}
}
`;
const signUpResponse = await apolloServer.executeOperation({
query: signUpInput,
variables: {
signUpInput: {
name: 'John',
lastName: 'Doe',
email: 'test#gmail.com',
password: 'password',
},
},
});
expect(signUpResponse.data).toBeDefined();
});
});
This PR https://github.com/nestjs/graphql/pull/1104 enables you to write tests using apollo-server-testing.

Resources