I have postgraphile running as an express middleware. For example:
const pgMiddleware = postgraphile(pool, SCHEMA, postgraphileConfig);
app.use(pgMiddleware);
How to get or intercept the result of a query or mutation without having a separate client?
For example when I send the below query
query {
personById(id: 1){
firstname
}
}
I want to be able to get the data sent back inside the same express app. How can I do that?
I believe what you are asking for is to be able to execute GraphQL operations against a PostGraphile schema from other routes/middlewares in Express without needing to make additional http requests. This is called schema only usage and you will specifically want to use withPostGraphileContext to execute your request and process results:
import type { Express } from "express";
import type { Pool } from "pg";
import {
gql,
makeProcessSchemaPlugin,
postgraphile,
withPostGraphileContext,
} from "postgraphile";
import PgSimplifyInflectorPlugin from "#graphile-contrib/pg-simplify-inflector";
import type { GraphQLSchema } from "graphql";
import { graphql } from "graphql";
// Register your middlewares with express
const schemaOnlyUsageApp = (app: Express, pool: Pool) => {
let schema: GraphQLSchema;
// This plugin will execute a callback each time the PostGraphile
// GraphQl schema is rebuit.
const schemaProcessorPlugin = makeProcessSchemaPlugin((newSchema) => {
schema = newSchema;
return schema;
});
// Register the PostGraphile middleware as normal for requests on /graphql (and /graphiql)
app.use(
postgraphile(pool, "my_schema", {
simpleCollections: "omit",
dynamicJson: true,
legacyRelations: "omit",
setofFunctionsContainNulls: false,
appendPlugins: [PgSimplifyInflectorPlugin, schemaProcessorPlugin],
watchPg: true,
graphiql: true,
enhanceGraphiql: true,
showErrorStack: true,
allowExplain: true,
})
);
// custom route that will execute a predefined gql query directly against the schema
app.get("/posts", async (req, res) => {
// arbitrary gql query
const query = gql`
query posts {
posts {
edges {
node {
id
title
body
likeCount
createdAt
}
}
}
}
`;
const result = await withPostGraphileContext(
{
// Reuse your pool to avoid creating additional connections
pgPool: pool,
},
async (context) => {
// execute your query directly and get results without making
// an additional http request!
const queryResult = await graphql({
schema,
source: query.loc?.source || "",
contextValue: { ...context },
});
return queryResult;
}
);
res.send(result);
});
};
export default schemaOnlyUsageApp;
Related
Environment
Node.js (18 LTS) / Express (^4.18.2)
MongoDB Native Driver (^4.12.0)
MongoDB Atlas (5.0.14)
Application Structure
.github
config
- mongodb_client.js
dist
middleware
node_modules
routes
src
views
.env
.gitignore
app.js
package.json
README.md
Connection Code
As a sanity check, this is the connection code that is provided in the MongoDB Atlas interface:
As a screenshot:
As code:
const { MongoClient, ServerApiVersion } = require('mongodb');
const uri = "mongodb+srv://admin:<password>#cluster0.******.mongodb.net/?retryWrites=true&w=majority";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
client.connect(err => {
const collection = client.db("test").collection("devices");
// perform actions on the collection object
client.close();
});
Desired Behaviour
The code snippet provided in the MongoDB Atlas interface performs the connection and subsequent database calls in one file.
However, I would like to:
Create a file that contains the MongoDB Atlas connection (e.g mongodb_client.js)
Export the connection so that it can be used in middleware files (e.g my_middleware_01.js)
So, in pseudo code, I imagine it would look something like this:
config / mongodb_client.js
import { MongoClient, ServerApiVersion } from 'mongodb';
const connection_string = process.env.MONGODB_CONNECTION_STRING;
const mongodb_client = new MongoClient(connection_string, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
// export the connection somehow
export { mongodb_client };
middleware / my_middleware_01.js
import { mongodb_client } from '../config/mongodb_client.js';
const api_myResource_get = async (req, res) => {
mongodb_client.open();
let collection = mongodb_client.db('myDatabase').collection('myCollection');
let result = await collection.findOne(query, options);
res.json({ result: result });
mongodb_client.close();
};
export { api_myResource_get };
What I've Tried
It seems I was grappling with this dynamic over a year ago and posted my solution here:
https://stackoverflow.com/a/70135909
However, I think that conventions have changed since then.
For example when instantiating the client, the current method seems to be:
const client = new MongoClient(connection_string, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
whereas previously it was something like:
await MongoClient.connect(connection_string);
I've Google searched:
how to make mongodb connection available in node.js middleware?
But all the results seem to reference this older convention and I'd like to ensure I am using best practice (and most recent conventions).
Related Questions and Resources
Passing Mongo DB Object DB to Express Middleware
What is best way to handle global connection of Mongodb in NodeJs
How do I manage MongoDB connections in a Node.js web application?
What is the difference between MongoClient and the client object which we get in the callback of MongoClient.connect() method
How to properly reuse connection to Mongodb across NodeJs application and modules
MongoDB Driver Connection Documentation
EDIT 01:
Below is one attempt which is returning the error:
TypeError: Cannot read properties of null (reading 'db')
config / mongodb_connection.js
import { MongoClient, ServerApiVersion } from 'mongodb';
const connection_string = process.env.MONGODB_CONNECTION_STRING;
class mongodb_connection {
static async open() {
if (this.conn) return this.conn;
this.conn = await MongoClient.connect(connection_string, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
return this.conn;
}
}
mongodb_connection.conn = null;
mongodb_connection.url = connection_string;
export { mongodb_connection };
middleware / api_mongodb_get.js
import { mongodb_connection } from '../../config/mongodb_connection.js';
const api_mongodb_get = async (req, res) => {
try {
mongodb_connection.open();
const collection = mongodb_connection.conn.db('pages').collection('pages');
const result = await collection.findOne({ "my_key": "my value" });
res.json({ data: result });
mongodb_connection.close();
} catch (error) {
console.error(error);
res.status(500).send(error);
}
};
export { api_mongodb_get };
EDIT 02:
The following 'works' but I don't know if it is best practice or not.
In other words, I don't know if I am overlooking something that will cause undesired behavior.
config / mongodb_connection.js
import { MongoClient, ServerApiVersion } from 'mongodb';
const connection_string = process.env.MONGODB_CONNECTION_STRING;
const mongodb_connection = new MongoClient(connection_string, { useNewUrlParser: true, useUnifiedTopology: true, serverApi: ServerApiVersion.v1 });
export { mongodb_connection };
middleware / api_mongodb_get.js
import { mongodb_connection } from '../../config/mongodb_connection.js';
const api_mongodb_get = async (req, res) => {
try {
mongodb_connection.connect(async err => {
const collection = mongodb_connection.db('pages').collection('pages');
const result = await collection.findOne({ "my_key": "my value" });
res.json({ data: result });
mongodb_connection.close();
});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
};
export { api_mongodb_get };
Insetead of using mongodb, use mongoose library to establish the connection.
Here is an example to establish the connection with mongodb cluster:
connectDb.js:
const dotenv = require('dotenv').config();
const DB_URL = process.env.DB_URL;
const mongoose = require('mongoose');
const connectDb = async () => {
try {
const connection = await mongoose.connect(DB_URL)
console.log(`Connected to database Successfully: ${connection}`)
} catch (error) {
console.log(error)
}
}
module.exports = connectDb;
and I think I don't need to mention that the DB_URL is the URL which is provided by the mondodb cluster.
I'm writing pact integration tests which require to perform actual call to specific mock server during running tests.
I found that I cannot find a way to change RTK query baseUrl after initialisation of api.
it('works with rtk', async () => {
// ... setup pact expectations
const reducer = {
[rtkApi.reducerPath]: rtkApi.reducer,
};
// proxy call to configureStore()
const { store } = setupStoreAndPersistor({
enableLog: true,
rootReducer: reducer,
isProduction: false,
});
// eslint-disable-next-line #typescript-eslint/no-explicit-any
const dispatch = store.dispatch as any;
dispatch(rtkApi.endpoints.GetModules.initiate();
// sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
const data = store.getState().api;
expect(data.queries['GetModules(undefined)']).toEqual({modules: []});
});
Base api
import { createApi } from '#reduxjs/toolkit/query/react';
import { graphqlRequestBaseQuery } from '#rtk-query/graphql-request-base-query';
import { GraphQLClient } from 'graphql-request';
export const client = new GraphQLClient('http://localhost:12355/graphql');
export const api = createApi({
baseQuery: graphqlRequestBaseQuery({ client }),
endpoints: () => ({}),
});
query is very basic
query GetModules {
modules {
name
}
}
I tried digging into customizing baseQuery but were not able to get it working.
I'm using apollo-server-express as so right now:
import { ApolloServer } from 'apollo-server-express';
import { WebApp } from 'meteor/webapp';
import { getUser } from 'meteor/apollo';
import schema from './api';
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const user = await getUser(req.headers.authorization);
return {
user,
};
},
playground: process.env.NODE_ENV === 'development',
introspection: true,
uploads: false,
});
server.applyMiddleware({
app: WebApp.connectHandlers,
path: '/graphql',
});
I'd like to add another field to the context which has all the query params from the URL my user is visiting.
It seems that the req object passed to the context function by apollo-server-express is of type express.Request which ought to have a req.query object. However, when I try to access that like so:
context: async ({ req }) => {
console.log('### query', req.query);
console.log('### params', req.params);
const user = await getUser(req.headers.authorization);
return {
user,
};
},
and visit my app at http://localhost:3000/u/3q2PcjRwyiqR2ywHM/BK2CiG7fN3P7Z5xvy?editToken=qdj3RRYjCuMxFNRnz (note the ?editToken=...)
I see the following log lines:
I20220417-15:08:54.477(-5)? ### query {}
I20220417-15:08:54.478(-5)? ### params undefined
I20220417-15:08:54.570(-5)? ### query {}
I20220417-15:08:54.571(-5)? ### params undefine
What is the correct way to access URL query params when creating the context for apollo-server-express? I'm specifically trying to add the editToken to the context.
Can anyone tell me how to implement GraphQL Subscriptions using Express-GraphQL in Node?
I have also run into the same problem. I wasn't able to find a clear solution to this in the documentation. So i have just switched to graphql-yoga instead. But i did find this thread so do check it out
I've been researching this same issue.
I've read the GitHub issues for express-graphql subscriptions and a member of that repo suggested using graphql-ws on the closing comment.
Here's a link to my GitHub project shammelburg/express-graphql-api, you can npm start load grapiql to test queries and mutation.
To test subscriptions, I've created an Angular project which implements graphql-ws's observables example. shammelburg/graphql-rxjs-angular
The Angular project also uses graphql-request for queries and mutations.
This is a very lightweight solution and works perfectly.
They've added the doc fragment mentioning Subscription Support with an example implementation in Nov 2020.
But unfortunately that never got released, there's an issue here mentioning that.
My workaround for now's been switching over to Express Playground for the subscriptions-transport-ws socket (Playground doesn't support graphql-ws yet) and Apollo Sandbox for the graphql-ws.
Then my subscription creation options are the following.
Where createScopedPermissionWrapper is just an execute wrapper with #graphql-authz and createGraphqlContext a factory function validating auth and creating a custom context for my resolvers.
import { Server } from 'http'
import { useServer } from 'graphql-ws/lib/use/ws' // subscription with graphql-ws
import { SubscriptionServer } from 'subscriptions-transport-ws' // subscription with subscriptions-transport-ws
export const createSubscriptionsTransportWs = (server: Server) => {
const wsServer = new SubscriptionServer(
{
schema,
execute: createScopedPermissionWrapper(),
subscribe,
onConnect: (args: { authentication?: string }) =>
createGraphqlContext({
authentication: args.authentication,
}),
},
{ server, path }
)
const wsAddress = wsServer.server.address() as AddressInfo
depClients.logger.success(
`Graphql subscription socket up on ${wsAddress.address}:${wsAddress.port}${path}`
)
}
export const createGraphqlWS = (server: Server) => {
const wsServer = new ws.Server({ server, path })
useServer(
{
schema,
execute: createScopedPermissionWrapper(),
subscribe,
context: (args: { connectionParams: { authentication?: string } }) =>
createGraphqlContext({
authentication: args.connectionParams.authentication,
}),
},
wsServer
)
const wsAddress = wsServer.address() as AddressInfo
depClients.logger.success(
`Graphql subscription socket up on ${wsAddress.address}:${wsAddress.port}${path}`
)
}
See Authentication and Express Middleware
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');
var schema = buildSchema(`
type Query {
ip: String
}
`);
const loggingMiddleware = (req, res, next) => {
console.log('ip:', req.ip);
next();
}
var root = {
ip: function (args, request) {
return request.ip;
}
};
var app = express();
app.use(loggingMiddleware);
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
I have an API build using sailsjs and a react redux attach to a nodejs backend, and i am trying to implement socket.io for a realtime communication, how does this work?
is it
socket.io client on the react side that connects to a socket.io server on its nodejs backend that connects to a socket.io server on the API
socket.io client on the react side and on its nodejs backend that connects to a socket.io server on the API
i have tried looking around for some answers, but none seems to meet my requirements.
to try things out, i put the hello endpoint on my API, using the sailsjs realtime documentation, but when i do a sails lift i got this error Could not fetch session, since connecting socket has no cookie (is this a cross-origin socket?) i figure that i need to pass an auth code inside the request headers Authorization property.
Assuming i went for my #1 question, and by using redux-socket.io,
In my redux middleware i created a socketMiddleware
import createSocketIoMiddleware from 'redux-socket.io'
import io from 'socket.io-client'
import config from '../../../config'
const socket = io(config.host)
export default function socketMiddleware() {
return createSocketIoMiddleware(
socket,
() => next => (action) => {
const { nextAction, shuttle, ...rest } = action
if (!shuttle) {
return next(action)
}
const { socket_url: shuttleUrl = '' } = config
const apiParams = {
data: shuttle,
shuttleUrl,
}
const nextParams = {
...rest,
promise: api => api.post(apiParams),
nextAction,
}
return next(nextParams)
},
)
}
and in my redux store
import { createStore, applyMiddleware, compose } from 'redux'
import createSocketIoMiddleware from 'redux-socket.io'
...
import rootReducers from '../reducer'
import socketMiddleware from '../middleware/socketMiddleware'
import promiseMiddleware from '../middleware/promiseMiddleware'
...
import config from '../../../config'
export default function configStore(initialState) {
const socket = socketMiddleware()
...
const promise = promiseMiddleware(new ApiCall())
const middleware = [
applyMiddleware(socket),
...
applyMiddleware(promise),
]
if (config.env !== 'production') {
middleware.push(DevTools.instrument())
}
const createStoreWithMiddleware = compose(...middleware)
const store = createStoreWithMiddleware(createStore)(rootReducers, initialState)
...
return store
}
in my promiseMiddleware
export default function promiseMiddleware(api) {
return () => next => (action) => {
const { nextAction, promise, type, ...rest } = action
if (!promise) {
return next(action)
}
const [REQUEST, SUCCESS, FAILURE] = type
next({ ...rest, type: REQUEST })
function success(res) {
next({ ...rest, payload: res, type: SUCCESS })
if (nextAction) {
nextAction(res)
}
}
function error(err) {
next({ ...rest, payload: err, type: FAILURE })
if (nextAction) {
nextAction({}, err)
}
}
return promise(api)
.then(success, error)
.catch((err) => {
console.error('ERROR ON THE MIDDLEWARE: ', REQUEST, err) // eslint-disable-line no-console
next({ ...rest, payload: err, type: FAILURE })
})
}
}
my ApiCall
/* eslint-disable camelcase */
import superagent from 'superagent'
...
const methods = ['get', 'post', 'put', 'patch', 'del']
export default class ApiCall {
constructor() {
methods.forEach(method =>
this[method] = ({ params, data, shuttleUrl, savePath, mediaType, files } = {}) =>
new Promise((resolve, reject) => {
const request = superagent[method](shuttleUrl)
if (params) {
request.query(params)
}
...
if (data) {
request.send(data)
}
request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body))
},
))
}
}
All this relation between the middlewares and the store works well on regular http api call. My question is, am i on the right path? if i am, then what should i write on this reactjs server part to communicate with the api socket? should i also use socket.io-client?
You need to add sails.io.js at your node server. Sails socket behavior it's quite tricky. Since, it's not using on method to listen the event.
Create sails endpoint which handle socket request. The documentation is here. The documentation is such a pain in the ass, but please bear with it.
On your node server. You can use it like
import socketIOClient from 'socket.io-client'
import sailsIOClient from 'sails.io.js'
const ioClient = sailsIOClient(socketIOClient)
ioClient.sails.url = "YOUR SOCKET SERVER URL"
ioClient.socket.get("SAILS ENDPOINT WHICH HANDLE SOCKET", function(data) {
console.log('Socket Data', data);
})