I am using TypeScript and have Server and Client application. Below is the server code.
Server Code
import express, { Express } from "express";
import { graphqlHTTP } from "express-graphql";
import { buildSchema } from "type-graphql";
import { TaskResolver } from "./resolvers/task.resolver";
import { pgDatasource } from "./configs/db.config";
import { SeatBandingResolver } from "./resolvers/seatBanding.resolver";
import { GuestChatResolver } from "./resolvers/guestChat.resolver";
import { RateResolver } from "./resolvers/rate.resolver";
import { YearResolver } from "./resolvers/year.resolver";
import { ImplementationRateResolver } from "./resolvers/implementationRate.resolver";
import { UserResolver } from "./resolvers/user.resolver";
import { ReportResolver } from "./resolvers/report.resolver";
// Subscriptions
const ws = require("ws");
const { useServer } = require("graphql-ws/lib/use/ws");
const { execute, subscribe } = require("graphql");
const main = async () => {
const app: Express = express();
try {
//connect to db
await pgDatasource.initialize();
} catch (err) {
throw err;
}
//build gql schema
let schema = await buildSchema({
resolvers: [
SeatBandingResolver,
GuestChatResolver,
RateResolver,
YearResolver,
ImplementationRateResolver,
UserResolver,
],
validate: false,
// pubSub: new PubSub()
});
let schemaDoc = await buildSchema({
resolvers: [ReportResolver],
validate: false,
});
//ql schema for report
const docServer = graphqlHTTP((req, res) => {
return {
schema: schemaDoc,
graphiql: true,
context: {
req: req,
header: req.headers,
},
};
});
//setting a graphql server instance
const graphqServer = graphqlHTTP((req, res, graphQLParams) => {
return {
schema,
context: {
req: req,
header: req.headers,
},
graphiql: true,
};
});
app.use(cors());
//graphql endpoint : change it to backend
app.use("/graphql", graphqServer);
//for report : change name to google api
app.use("/doc", docServer);
//test route
app.get("/", (req, res) => {
res.json({
message: "Hello world",
});
});
let server = app.listen(3001, () => {
console.log("server started");
const wsServer = new ws.WebSocketServer({
host: "localhost",
// server,
path: "/graphql",
port: 3001,
});
useServer(
{
schema,
execute,
subscribe,
onConnect: (ctx) => {
console.log("Connect");
},
onSubscribe: (ctx, msg) => {
console.log("Subscribe");
},
onNext: (ctx, msg, args, result) => {
console.debug("Next");
},
onError: (ctx, msg, errors) => {
console.error("Error");
},
onComplete: (ctx, msg) => {
console.log("Complete");
},
},
wsServer
);
});
};
//starting a server
main()
.then(async (_) => {
// await addColumn()
})
.catch((err) => {
console.log(err);
});
Subscription Code at Client Side
import { Year } from "../entities/year.entity";
import { NewYear } from "../inputs/addYear.input";
import {
Arg,
Ctx,
Field,
Int,
Mutation,
ObjectType,
Query,
Resolver,
Root,
Subscription,
UseMiddleware,
} from "type-graphql";
import { Request } from "express";
import { Response } from "../helpers/response.helper";
import { Pagination } from "../inputs/pagination.input";
import { isAuth } from "../helpers/auth.helper";
import { PubSub, PubSubEngine } from "graphql-subscriptions";
const pubSub = new PubSub();
#ObjectType()
class MessagePayload {
#Field()
message: string;
}
#Resolver(() => Year)
export class YearResolver {
#Mutation(() => String)
async sendMessage(#Arg("message") message: string): Promise<string> {
console.log("in send subscription");
pubSub.publish("MESSAGE_NOTIFICATION", { message });
return message;
}
//calling the subscription
#Subscription(() => MessagePayload || null, {
topics: "MESSAGE_NOTIFICATION",
})
async receiveMessage(
#Root() root: MessagePayload
): Promise<MessagePayload | null> {
console.log("in publisher");
console.log(root, "in recieve message");
pubSub.asyncIterator("MESSAGE_NOTIFICATION");
return { message: "hello from the subscription" };
}
}
The issue I am facing here is Subscription is not working properly and the data is always null.
Can anyone help me to identify what I am missing here?
Thanks.
I'm not sure for 100% because your code descriptions are kinda confusing, but it looks like you should return pubSub.asyncIterator('MESSAGE_NOTIFICATION') in method receiveMessage. This method is called to start streaming messages to client at selected channel (MESSAGE_NOTIFICATION), not sending them. To send messages use pubsub. Of course you should change typing too.
You can find a similiar implementation here.
Related
This is the index.ts file:
import mysql from "mysql2";
import * as dotenv from "dotenv";
dotenv.config({ path: __dirname+'/.env' });
import { app } from "./app";
import {Pools} from "./config/db";
const start = async () => {
const default_options = {
host: 'localhost', //(process.env.DB_HOST),
user: 'a', //(process.env.DB_USER),
password: '11111', // (process.env.DB_PASSWORD ),
database: 'users_auth', //(process.env.DB_NAME ),
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
};
try {
await Pools.connect(default_options);
console.log("Connected to MySQL");
} catch (err) {
console.log(err);
}
app.listen(3000, () => {
console.log("Listening on port 3000!!!!!!!!");
});
};
start();
I have the following /config/db file:
import mysql from "mysql2";
import { Pool } from "mysql2/promise";
class Pools {
static _pools: Pool;
static connect(options: mysql.PoolOptions) {
this._pools = mysql.createPool(options).promise();
this._pools.execute('SELECT 1 + 1;');
}
static close() {
this._pools.end();
}
static execute(sql: string, params?: any[] | undefined) {
this._pools.execute(sql, params);
}
}
export { Pools };
And the following userController file:
import {Pools} from "../config/db";
class User {
static async findAll(req: Request, res: Response, next: NextFunction) {
await Pools.execute("SELECT * FROM users" )
.then((rows) => {
res.send(rows[0]);
})
.catch(err => console.log(err));
next();
}
}
The problem is that I get the following error at this line .then((rows) => { within the userController file:
Property 'then' does not exist on type 'void'.ts(2339)
I am fairly new to using graphql-ws and graphql-yoga server, so forgive me if this is a naive question or mistake from my side.
I went through graphql-ws documentation. It has written the schema as a parameter. Unfortunately, the schema definition used in the documentation is missing a reference.
After adding a new todo (using addTodo) it shows two todo items. So I believe it is unable to return the initial todo list whenever running subscribe on Yoga Graphiql explorer.
It should show the initial todo item as soon as it has been subscribed and published in the schema definition.
My understanding is there is something I am missing in the schema definition which is not showing the todo list when tried accessing Yoga Graphiql explorer.
Has anyone had a similar experience and been able to resolve it? What I am missing?
Libraries used
Backend
graphql-yoga
ws
graphql-ws
Frontend
solid-js
wonka
Todo item - declared in schema
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
Screenshot
Code Snippets
Schema definition
import { createPubSub } from 'graphql-yoga';
import { Todo } from "./types";
let todos = [
{
id: "1",
title: "Learn GraphQL + Solidjs",
completed: false
}
];
// channel
const TODOS_CHANNEL = "TODOS_CHANNEL";
// pubsub
const pubSub = createPubSub();
const publishToChannel = (data: any) => pubSub.publish(TODOS_CHANNEL, data);
// Type def
const typeDefs = [`
type Todo {
id: ID!
title: String!
completed: Boolean!
}
type Query {
getTodos: [Todo]!
}
type Mutation {
addTodo(title: String!): Todo!
}
type Subscription {
todos: [Todo!]
}
`];
// Resolvers
const resolvers = {
Query: {
getTodos: () => todos
},
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
const res = pubSub.subscribe(TODOS_CHANNEL);
publishToChannel({ todos });
return res;
}
},
},
};
export const schema = {
resolvers,
typeDefs
};
Server backend
import { createServer } from "graphql-yoga";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { schema } from "./src/schema";
import { execute, ExecutionArgs, subscribe } from "graphql";
async function main() {
const yogaApp = createServer({
schema,
graphiql: {
subscriptionsProtocol: 'WS', // use WebSockets instead of SSE
},
});
const server = await yogaApp.start();
const wsServer = new WebSocketServer({
server,
path: yogaApp.getAddressInfo().endpoint
});
type EnvelopedExecutionArgs = ExecutionArgs & {
rootValue: {
execute: typeof execute;
subscribe: typeof subscribe;
};
};
useServer(
{
execute: (args: any) => (args as EnvelopedExecutionArgs).rootValue.execute(args),
subscribe: (args: any) => (args as EnvelopedExecutionArgs).rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } =
yogaApp.getEnveloped(ctx);
const args: EnvelopedExecutionArgs = {
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);
});
apply these changes
Mutation: {
addTodo: (_: unknown, { title }: Todo) => {
const newTodo = {
id: "" + (todos.length + 1),
title,
completed: false
};
todos.push(newTodo);
publishToChannel({ todos });
return newTodo;
},
Subscription: {
todos: {
subscribe: () => {
return Repeater.merge(
[
new Repeater(async (push, stop) => {
push({ todos });
await stop;
}),
pubSub.subscribe(TODOS_CHANNEL),
]
)
}
},
},
first, npm i #repeaterjs/repeater then import Repeater
I'm having a super hard time with setting up subscriptions in apollo-server. It's listening, I can make the mutation and query the result. But, the subscription for the same data returns null every time. Here are the important parts of my code:
import { Storage } from "#google-cloud/storage"
import { createError } from "apollo-errors"
import { ApolloServer, gql, PubSubEngine } from "apollo-server-express"
import { PubSub } from "apollo-server"
import cors from "cors"
import express from "express"
import jwt from "express-jwt"
import jwksRsa from "jwks-rsa"
import neo4j from "neo4j-driver"
import { initializeDatabase } from "./initialize"
import { typeDefs } from "./schema.js"
import { createServer } from "HTTP"
const pubsub = new PubSub()
const app = express()
const messages = []
const resolvers = {
Mutation: {
postMessage(parent, { user, content }, context) {
const id = messages.length
messages.push({
id,
user,
content,
})
pubsub.publish("test", {
messages,
})
return id
},
},
Query: {
messages: () => messages,
},
Subscription: {
messages: {
subscribe: (parent, args, context) => {
return pubsub.asyncIterator("test")
},
},
},
}
const neoSchema = new Neo4jGraphQL({
typeDefs,
resolvers,
config: {
jwt: {
secret: process.env.NEO4J_GRAPHQL_JWT_SECRET,
},
},
debug: true,
})
const NEO4J_USER = process.env.NEO4J_USER
const NEO4J_PASS = process.env.NEO4J_PASS
const NEO4J_URI = process.env.NEO4J_URI
const driver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USER, NEO4J_PASS))
const init = async (driver) => {
await initializeDatabase(driver)
}
try {
init(driver).catch((e) => console.log(e))
console.log("ne4j initialized...")
} catch (error) {
console.error(`Failed to property initialize database`, error)
}
const apolloServer = new ApolloServer({
schema: neoSchema.schema,
context: ({ req }) => {
return {
driver,
pubsub,
req,
}
},
subscriptions: {
onConnect: async (connectionParams, webSocket, context) => {
console.log("xxx")
console.log(connectionParams)
},
onDisconnect: (websocket, context) => {
console.log("WS Disconnected!")
},
path: "/graph",
},
introspection: true,
playground: true,
})
apolloServer.applyMiddleware({ app, path: "/graph" })
const httpServer = createServer(app)
apolloServer.installSubscriptionHandlers(httpServer)
const port = process.env.PORT || 8080
httpServer.listen({ port }, () => {
console.log(`Server is ready at http://localhost:${port}${apolloServer.graphqlPath}`)
console.log(`Subscriptions ready at ws://localhost:${port}${apolloServer.subscriptionsPath}`)
})
And from my schema:
export const typeDefs = gql`
type Message {
id: ID!
user: String!
content: String!
}
type Query {
messages: [Message!]
}
type Subscription {
messages: [Message!]
}
type Mutation {
postMessage(user: String!, content: String!): ID!
}
`
If I execute a mutation on postMessage, then query messages, I get the expected data back. However, when I run the subscription from the client, I get
{
"data": {
"messages": null
}
}
What's happening here? What am I missing? Is there something missing in my server setup? Are my pubsub and subscriptions code off?
I need to do a server-to-server graphQL call. Thanks to the advice received on this SO post, and also documented here, I'm approaching it like this:
async function () {
const {data, errors} = await graphql(
schema,
CRON_JOB_TO_FIND_USERS_WHO_HAVE_GONE_OFFLINE_MUTATION,
{},
{caller: 'synced-cron'},
{timeStarted: new Date().toISOString().slice(0, 19).replace('T', ' ')}
)
console.log('data', data)
console.log('errors', errors)
return true;
}
It's not throwing any errors, but it's returning null data:
Also, a debugger breakpoint in the resolver isn't being hit.
SCHEMA
cronJobToFindUsersWhoHaveGoneOffline(timeStarted: String): epUserData
QUERY
// note -- no gql``. This string is passed directly to graphql() function
// where it gets gql applied to it.
const CRON_JOB_TO_FIND_USERS_WHO_HAVE_GONE_OFFLINE_MUTATION = `
mutation ($timeStarted: String){
cronJobToFindUsersWhoHaveGoneOffline(timeStarted: $timeStarted){
id,
user_presence,
user_presence_time_of_last_update
},
}
`;
RESOLVER
cronJobToFindUsersWhoHaveGoneOffline(parent, args, context){
debugger; <== NEVER GETS ACTIVATED
return Promise.resolve()
.then(() => {
debugger;
//CODE TO FIND USERS AND MARK THEM AS BEING OFFLINE GOES HERE
return usersWhoWentOffline;
})
.then((usersWhoWentOffline) => {
debugger;
return usersWhoWentOffline;
})
.catch((err) => {
debugger;
console.log(err);
});
},
What am I missing?
It should work. Here is a completed working example:
server.ts:
import { ApolloServer } from 'apollo-server';
import { schema } from './schema';
const server = new ApolloServer({ schema });
export { server };
schema.ts:
import { gql, makeExecutableSchema } from 'apollo-server';
const typeDefs = gql`
type EpUserData {
id: ID!
user_presence: String
user_presence_time_of_last_update: String
}
type Query {
dummy: String
}
type Mutation {
cronJobToFindUsersWhoHaveGoneOffline(timeStarted: String): EpUserData
}
`;
const resolvers = {
Mutation: {
async cronJobToFindUsersWhoHaveGoneOffline(parent, args, context) {
const usersWhoWentOffline = { id: 1, user_presence: 'test', user_presence_time_of_last_update: '2020' };
return Promise.resolve()
.then(() => {
return usersWhoWentOffline;
})
.catch((err) => {
console.log(err);
});
},
},
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
export { schema };
server.test.ts:
import { graphql } from 'graphql';
import { schema } from './schema';
import { server } from './server';
const CRON_JOB_TO_FIND_USERS_WHO_HAVE_GONE_OFFLINE_MUTATION = `
mutation ($timeStarted: String){
cronJobToFindUsersWhoHaveGoneOffline(timeStarted: $timeStarted){
id,
user_presence,
user_presence_time_of_last_update
},
}
`;
describe('62122142', () => {
beforeAll(async () => {
const { url } = await server.listen();
console.log(`server is listening on ${url}`);
});
afterAll(async () => {
await server.stop();
});
it('should pass', async () => {
const { data, errors } = await graphql(
schema,
CRON_JOB_TO_FIND_USERS_WHO_HAVE_GONE_OFFLINE_MUTATION,
{},
{ caller: 'synced-cron' },
{
timeStarted: new Date()
.toISOString()
.slice(0, 19)
.replace('T', ' '),
},
);
console.log('data', data);
console.log('errors', errors);
return true;
});
});
integration test result:
PASS apollo-graphql-tutorial src/stackoverflow/62122142/server.test.ts (7.143s)
62122142
✓ should pass (12ms)
console.log src/stackoverflow/62122142/server.test.ts:18
server is listening on http://localhost:4000/
console.log src/stackoverflow/62122142/server.test.ts:36
data [Object: null prototype] {
cronJobToFindUsersWhoHaveGoneOffline:
[Object: null prototype] {
id: '1',
user_presence: 'test',
user_presence_time_of_last_update: '2020' } }
console.log src/stackoverflow/62122142/server.test.ts:37
errors undefined
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 7.226s
source code: https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/stackoverflow/62122142
So I have written a custom polling hook which uses useContext and useLazyQuery hooks. I want to write a unit test for this, which should cover its returned values state and side effect.
So far I have managed to do this much but I'm not so sure how to proceed ahead. Any tips?
export const useUploadActivityPolling = (
teId: TeIdType
): UploadActivityPollingResult => {
const { dispatch, uploadActivityId }: StoreContextType = useAppContext();
const [fetchActivityStatus, { error: UploadActivityError, data: UploadActivityData, stopPolling }] = useLazyQuery(
GET_UPLOAD_ACTIVITY,
{
pollInterval: 3000,
fetchPolicy: 'network-only',
variables: { teId, activityId: uploadActivityId },
}
);
useEffect(() => {
if (UploadActivityData) {
setUploadActivityId(
UploadActivityData.getUploadActivityStatus.activity_id,
dispatch
);
updateActivityStateAction(UploadActivityData.getExcelUploadActivityStatus.status, dispatch);
}
}, [UploadActivityData]);
return { fetchActivityStatus, stopPolling, UploadActivityError };
};
import React from 'react';
import { mount } from 'enzyme';
const TestCustomHook = ({ callback }) => {
callback();
return null;
};
export const testCustomHook = callback => {
mount(<TestCustomHook callback={callback} />);
};
describe('useUploadActivityPolling', () => {
let pollingResult;
const teId = 'some id';
beforeEach(() => {
testCustomHook(() => {
pollingResult = useUploadActivityPolling(teId);
});
});
test('should have an fetchActivityStatus function', () => {
expect(pollingResult.fetchActivityStatus).toBeInstanceOf(Function);
});
});