Meteor create --apollo, Not Yet Compatible with Meteor 3.0?

@storyteller, I have a PR for [Meteor 3] Apollo skeleton upgrade.

  • Updated for Meteor 3rc0
  • Does not require Meteor package swydo:apollo
  • Demonstrates that Meteor.user() cannot yet be accessed in resolvers

Do I need a username/password to submit it as a pull request?

1 Like

You can open a PR to the Meteor repository. Check here how you can do it

1 Like

Hmmmm… I haven’t modified the Meteor code that runs when `meteor create apollo-app --apollo is run - only the final app, e.g. “apollo-app”. So really this wouldn’t be a pull request at all.

Uploads here don’t permit .zip files. Where is a good place for me to upload my (very slightly) modified apollo-app?

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request

Or just commit your changes into a branch and share the branch here

Thanks!

If you are suggesting an update, why are you not creating a PR? I am actually confused on what you wanted to do

Great question. I have two goals:

  1. Provide an easy demo to see that Meteor.user() is not yet accessible in resolvers, as it has been with Meteor v1 and v2.
  2. Show an easy way to remove swydo:apollo from the Apollo skeleton app.

Help the devs who will review/check this easy way that you created. How can you help them? With a PR. At the very least, create a PR of the change itself to quickly see what you changed. As of now, you do not even have a PR to review on your own branch on what you did change.

Right you are. Here is a repo with main == meteor create -apollo untouched, with a second commit with the minor changes.

Here’s what you see when you run it:

1 Like

Hi @vikr00001,

Everything here seems correct… You get this exact behavior in version 2 as well.

Your own reproduction is on version 2.15, actually. I had to update it to version 3 to simulate the issue, and it behaives the same.

To have the user inside the getLink resolver, you can do something like:

    getLink: async (obj, { id }) => {
      const user = await Meteor.users.findOneAsync();
      console.log(user)
      return LinksCollection.findOneAsync(id)
    },

Thanks for the correction!

I’ll come back with any additional questions.

I have this Apollo startup code running on my server:

const { url } = await startStandaloneServer(server, {
    context: async () => {
        const { cache } = server;
        try {
            return {
                user: await Meteor.userAsync(),
                cache
            };
        } catch (error) {
            console.error('Error fetching user:', error);
        }
    },
});

I’m getting the error:

Error: Meteor.userId can only be invoked in method calls or publications.

Do I need to run it inside of another context, e.g., Meteor. startup()?

UPDATE: Should this work? (I can’t test it yet in Mv3.)

Meteor.methods({
    'getUserId'() {
        return Meteor.userId()
    }
});
const { url } = await startStandaloneServer(server, {
    context: async (params) => {
        const userId = await Meteor.callAsync(
            "getUserId"
        );
        const { cache } = server;
        try {
            return {
                userId: userId,
                cache
            };
        } catch (error) {
            console.error('Error fetching user or userId:', error);
        }
    },
});

UPDATE #2 I’ve gotten this code to where I can test it.

        const userId = await Meteor.callAsync(
            "getUserId"
        );

…does not throw, but it doesn’t seem to return the Meteor user id yet, even if a user is logged in. ???

It seems that this should work. I can do some tests as well, but just to be clear, do you need to get the user ID on the server side inside a resolver, right?

Does the code you sent above work on version 2? If yes, can you create a reproduction of this same scenario working with version 2?

Working on a reproduction now.

Here’s a repo. This is still Meteor 2.16. There are a few questions that might be helpful to resolve prior to updating this to Meteor 3:

The existing Meteor Apollo skeleton seeks to add Meteor user to Apollo Context, but the Context object does not appear to be accessible from the resolvers. See: imports/apollo/resolvers.js:

const resolvers = {
    Query: {
        getLink: async function findLinkById(parent, args, contextValue) {
            // context not available here?
            return await LinksCollection.findOne(args.id);
        },
        getLinks: async function (parent, args, contextValue) {
            // context not available here?
            return await LinksCollection.find().fetch()
        }
    }
};

Context should be the third param passed to a resolver. But it has no defined value in the resolvers above.

Putting a call to Meteor.user() inside a resolver does not work in Meteor 2.x as I had thought. I’ve been using it there for years, so possibly this is something swydo:ddp-apollo enabled. Totally okay though, because we are trying to pass that data to the resolvers via context.

Which brings us to setup for context. Please see server/apollo.js:

Meteor.methods({
    'getUserId'() {
        let userId = null
        try {
            userId = Meteor.userId()
        } catch (error) {
            console.log('getUserId: ', error)
        }
        return userId
    }
});

const context = async function ({req}) {
    let userTest = await getUser(req.headers.authorization)
    let userTest2 = Meteor.call(
        "getUserId"
    );
    let userTest3 = await Meteor.users.findOneAsync();
    return {
        user: await getUser(req.headers.authorization)
    };
};

Note: this only runs when a graphQL query runs.

As you can see, I’m trying several tests here to obtain Meteor.user data. At the moment, they are returning as follows:

  • userTest: undefined
  • userTest2: null
  • userTest3: undefined

For all I know, all 3 are correct, since there is no logged-in user. The skeleton app has no login function.

Is there a way to determine if one or more of these are working?

UPDATE: This same Apollo setup does communicate context to my resolvers in my Mv3 app. So probably it requires the latest node and/or apollo packages. Creating an Mv3 version of this repo now…

UPDATE 2: The Mv3 repo does communicate context to the resolver!

Once we figure out how to get Meteor.user() into context, we will be there!

The code that needs to be iterated is at server/apollo.js.

@ denyhs, I don’t know Meteor v3 well enough to think of ways to look into this. Your advice will be greatly appreciated.

Note: had the wrong link to the Mv3 repo before. Fixed now.

Thanks for the details! I’m going to have a look at everything later.

1 Like

swydo:ddp-apollo has this interesting approach to getting Meteor user:

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;
}
import { getUserIdByLoginToken, NO_VALID_USER_ERROR } from './getUserIdByLoginToken';

export function meteorAuthMiddleware(req, res, next) {
  let tokenType;
  let loginToken;

  // get the login token from the request headers, given by meteorAuthLink
  if (req.headers && req.headers.authorization) {
    const parts = req.headers.authorization.split(' ');
    [tokenType, loginToken] = parts;

    if (parts.length !== 2 || tokenType !== 'Bearer') {
      // Not a valid login token, so unset
      loginToken = undefined;
    }
  }

  // get the user for the context
  getUserIdByLoginToken(loginToken)
    .then((userId) => {
      req.userId = userId;
      next();
    })
    .catch((err) => {
      err === NO_VALID_USER_ERROR ? next() : next(err);
    });
}

Did you try the swydo:ddp-apollo approach to see if it works?

About calling the method getUserId, I checked your reproduction, and it doesn’t have login, so it makes sense it never returns a user. But I’m guessing your shared results were gathered from your running app, right?

Did you try the swydo:ddp-apollo approach to see if it works?

Yes, I tried it in the Mv3 version of my running app, and it did not appear to work yet. It may depend on other parts of swydo:ddp-apollo, or of the Swydo Meteor package.

I’ve updated the repo to include a new branch (origin/adding_meteor_accounts) with rudimentary Meteor account creation code. On creating an account, the new user becomes logged in, and can be used for testing purposes.

Interesting – in this code:

async function startApolloServer() {
    const {url} = await startStandaloneServer(server, {
        context: async ({req}) => {
            let userTest = null;
            let userTest2 = null;
            let userTest3 = null;
            let userId = null;
            try {
                userTest = await getUser(req.headers.authorization)
                userTest2 = await Meteor.call(
                    "getUserId"
                );
                userTest3 = await Meteor.users.findOneAsync();
                userId = userTest3._id;
            } catch (error) {
                console.log('context: ', error)
            }
            return {
                userId: userId
            };
            return {
                userId,
            };
        },
    });

userTest3 = await Meteor.users.findOneAsync(); is working. But I think it’s just returning the first user found, rather than the currently logged-in user on the client.

Given that I can access Meteor.users.findOneAsync(); is there another call that would be expected to be available, that can access the currently logged-in user?

UPDATE: In the same code, in this section:

async function startApolloServer() {
    const {url} = await startStandaloneServer(server, {
        context: async ({req}) => {

req is coming in with req.headers but not with req.headers.authorization. If I can get req.headers.authorization, then this might work:

userTest = await getUser(req.headers.authorization)