Possible to Get Meteor's MongoDB Instance?

I’m thinking of using the npm package, graphql-mongodb-subscriptions, with ApolloGraphQL. It wants me to initialize it with an instance of MongoDB:

const pubsub = new MongodbPubSub(connectionDb);

Is there a way to get access to Meteor’s MongoDB instance, and store it in a variable named connectionDb?

Or, should I create a new MongoDB client, like this?

const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';

const mongoClient = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });

const pubsub = new MongodbPubSub(mongoClient);

There is raw database API, I would try digging around the Mongo package, there should be some option to get the client as well (I do recall this being discussed in the past as well):

I do also recall seeing a GraphQL Meteor package that allows you to use the existing Meteor’s web sockets for subscriptions, but I think it is outdated, but the approach might still be salvageable.

1 Like

There is this amazing package, swydo:ddp-apollo, which makes everything about Apollo setup in Meteor effortless. It includes:

  • npm package swydo:ddp-apollo
  • Meteor packages swydo:ddp-apollo and swydo:graphql

I used it for years, but it needs an update to remove fibers.

I’m not skilled enough to update this myself. I could probably be part of a team of 2-3 developers who could do it, if there is interest from others in updating this package.

1 Like

I’m using updated version of swydo:graphql locally. I will see what I can do to help push an update for this. The other one I will have to look into, but I think that if I can get one, I should be able to get the other one as well.

That would be awesome. :+1: It would save other Meteor users the weeks of work that I’ve put in getting ApolloClient and ApolloServer setups that work and that send the value of Meteor.userId() to the resolvers in the context variable.

I do have, after several weeks, ApolloClient and ApolloServer setups that work, and that can even deliver the value of Meteor.user() and Meteor.userId() to the resolver in the context variable. I have a version that supports subscriptions, and another version that does not. Both work in Mv3. I can contribute these if that would be helpful.

Swydo:ddp-apollo somehow supports direct access to Meteor.user() and Meteor.userId() from inside the resolvers, which is nice but not essential now that I can send them in the context variable to the resolvers.

The one thing Swydo:ddp-apollo does not do, is set up a production pubsub library. That’s the part I’m working on now.

Do you use a pubsub library by any chance? If so could you please share the setup code?

You get the user document when using the official Apollo package. See the Apollo skeleton.

Yes, that is one thing that is so great about swydo - it sends the userId to the resolver in the context variable. It took me weeks to get that working, and the only way I did it was via code from swydo, plus some great advice from @minhna about where to find the Meteor token on the client.

Here’s my current setup code. It supports queries as well as subscriptions, and uses a function from swydo to send the value of Meteor.userId() to the resolver in the context variable.

APOLLO SERVER SETUP

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import cors from 'cors';
import typeDefs from '/imports/apollo/schema';
import {resolvers} from "../imports/apollo/resolvers";
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { createServer } from 'http';
import {getUserIdByLoginToken} from "../imports/apollo/meteor-apollo-utils";

const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
    typeDefs,
    resolvers,
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
    '/graphql',
    cors(),
    express.json(),
    expressMiddleware(server, {
        context: async ({req}) => {
            let token = req.headers['token']
            let userId = null;
            try {
                if (token !== "null") {
                    userId = await getUserIdByLoginToken(token);
                }
            } catch (error) {
                console.log('context: ', error)
            }
            return {
                userId: userId
            };
        },

    }),
);

await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000/graphql`);

APOLLO CLIENT

import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, ApolloProvider } from "@apollo/client";

// Create an HttpLink pointing to your GraphQL endpoint
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });

// Middleware to add custom headers
const customHeadersMiddleware = new ApolloLink((operation, forward) => {
    // Define your custom headers
    const customHeaders = {
        "token": localStorage.getItem("Meteor.loginToken")
    };

    // Use operation.setContext to add the custom headers to the request
    operation.setContext(({ headers }) => ({
        headers: {
            ...headers,
            ...customHeaders,
        },
    }));

    return forward(operation);
});

// Combine the middleware with the HttpLink
const apolloClient = new ApolloClient({
    link: customHeadersMiddleware.concat(httpLink),
    cache: new InMemoryCache(),
});

export {apolloClient};

FUNCTION GetUserIdByLoginToken
from swydo:ddp-apollo

//this code was found in swydo:ddp-apollo

const USER_TOKEN_PATH = 'services.resume.loginTokens.hashedToken';
export const NO_VALID_USER_ERROR = new Error('NO_VALID_USER');

export async function getUserIdByLoginToken(loginToken) {
    // `accounts-base` is a weak dependency, so we'll try to require it
    // eslint-disable-next-line global-require, prefer-destructuring
    const { Accounts } = require('meteor/accounts-base');

    if (!loginToken) { throw NO_VALID_USER_ERROR; }

    if (typeof loginToken !== 'string') {
        throw new Error("GraphQL login token isn't a string");
    }

    // the hashed token is the key to find the possible current user in the db
    const hashedToken = Accounts._hashLoginToken(loginToken);

    // get the possible user from the database with minimal fields
    const fields = { _id: 1, 'services.resume': 1 };
    const user = await Meteor.users.rawCollection()
        .findOne({ [USER_TOKEN_PATH]: hashedToken }, { fields });

    if (!user) { throw NO_VALID_USER_ERROR; }

    // find the corresponding token: the user may have several open sessions on different clients
    const currentToken = user.services.resume.loginTokens
        .find((token) => token.hashedToken === hashedToken);

    const tokenExpiresAt = Accounts._tokenExpiration(currentToken.when);
    const isTokenExpired = tokenExpiresAt < new Date();

    if (isTokenExpired) { throw NO_VALID_USER_ERROR; }

    return user._id;
}

I haven’t yet got pubsub working in the resolvers.

Here’s how schema can be structured, using gql, so as to be imported without using the swydo Meteor packages.

//note: use .js extension on filename rather than .graphql extension, e.g. `schema.js`.

import gql from "graphql-tag";

const typeDefs = gql`#graphql
type Link {
    _id: ID!
    title: String
    url: String
}

type Query {
    getLink (id: ID!): Link
    getLinks: [Link]
}`

export default typeDefs;

The author of apollo-graphql-mongodb responded to an issue I filed, with a link to an ApolloServer setup he provides, that includes a mongo-based pubsub!

It seems to be working as expected. Here it is with minor edits to connect to the Meteor MongoDb instance:

// npm install @apollo/server express graphql cors
import { ApolloServer } from '@apollo/server';
import { ExpressContextFunctionArgument, expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import bodyParser from 'body-parser';
import cors from 'cors';
import { readFileSync } from 'fs';
import path from 'path';
import * as url from 'url';
import mongoose from 'mongoose';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { createServer } from 'http';
import { MongodbPubSub } from 'graphql-mongodb-subscriptions';
import {getUserIdByLoginToken} from "./utils_server/meteor-apollo-utils";
import typeDefs from "./api/schema";
import {resolvers} from "./api/resolvers";


const PORT = 4000;
const MONGODB_URI = `mongodb://localhost:3001/meteor`;

const connectToDb = async () => {
    await mongoose.connect(MONGODB_URI);
};

await connectToDb();
console.log('🎉 Connected to database successfully');

const mongodbpubsub = new MongodbPubSub({
    connectionDb: mongoose.connections[0].db
});


// Create schema, which will be used separately by ApolloServer and
// the WebSocket server.
const schema = makeExecutableSchema({
    typeDefs,
    resolvers
});

// Create an Express app and HTTP server; we will attach the WebSocket
// server and the ApolloServer to this HTTP server.
const app = express();
const httpServer = createServer(app);

// Set up WebSocket server.
const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql'
});
const serverCleanup = useServer({
    schema,
    context: ({req})  => {
        return { pubsub: mongodbpubsub };
    }
}, wsServer);

// Set up ApolloServer.
const server = new ApolloServer({
    schema,
    plugins: [
        // Proper shutdown for the HTTP server.
        ApolloServerPluginDrainHttpServer({ httpServer }),

        // Proper shutdown for the WebSocket server.
        {
            async serverWillStart() {
                return {
                    async drainServer() {
                        await serverCleanup.dispose();
                    }
                };
            }
        }
    ]
});

await server.start();
app.use(
    '/graphql',
    cors(),
    bodyParser.json(),
    expressMiddleware(server, {
        // Adding a context property lets you add data to your GraphQL operation contextValue
        // @ts-ignore
        context: async (ctx, msg, args) => {
            // You can define your own function for setting a dynamic context
            // or provide a static value
            // return getDynamicContext(ctx, msg, args);
            let token = ctx.req.headers['token']
            let userId = null;
            try {
                if (token !== "null") {
                    userId = await getUserIdByLoginToken(token);
                }
            } catch (error) {
                console.log('context: ', error)
            }

            let clientIp = '';
            try{
                clientIp = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
            }
            catch{
                console.log("Couldn't get clientIp in ctx.req")
            }

            return {
                userId: userId,
                clientIp: clientIp,
                pubsub: mongodbpubsub
            };
        }
    }));

// Now that our HTTP server is fully set up, actually listen.
httpServer.listen(PORT, () => {
    console.log(`🚀 Query endpoint ready at http://localhost:${PORT}/graphql`);
    console.log(`🚀 Subscription endpoint ready at ws://localhost:${PORT}/graphql`);
});



// https://github.com/mjwheatley/graphql-mongodb-subscriptions/issues/43
// https://github.com/mjwheatley/apollo-graphql-mongodb/blob/main/src/index.ts
// https://www.apollographql.com/docs/apollo-server/migration/#migrate-from-apollo-server-express
// …in the section following the text:
//     The context function's syntax is similar for the expressMiddleware function:
// https://www.apollographql.com/docs/apollo-server/data/subscriptions
// https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries

Seems overly complicated compare to just using getUser from meteor/apollo and then getting user?._id from context.

Agreed. But I haven’t been able to get that to work in Mv3 yet without Swydo, and Swydo requires fibers. If you’ve gotten it to work, please let me know. :slight_smile:

Yes, I have a PR there:

Excellent. How do I install a pull request on my Meteor app? i.e. I I can’t Meteor npm install it. Do I download it and manually drop it into the node_modules folder?

P.S. – what’s the correct method to download a pull request that hasn’t been merged yet? I’m on this page but don’t yet see a download button.

Oh I see – that’s a Meteor package. Is the Meteor package swydo:ddp-apollo still needed? How about the npm package swydo:ddp-apollo?

If I download the files for your pull request of the Meteor package, swydo:graphql, where do I put them? I checked ~/.meteor/packages, but that seems to be file rather than a folder.

You download the PR code into packages folder in your app. That is how I have it running. Packages that are in your packages folder take precedent over any other source.

So while waiting to for these to be merges and working on them myself my package folder has grown quiet a lot:

Hmmm… I don’t see a packages folder. I’ll ask about this in a separate thread.