Skip to content
This repository has been archived by the owner on Jan 15, 2020. It is now read-only.

Commit

Permalink
add more comments & remove dependency on underscore
Browse files Browse the repository at this point in the history
  • Loading branch information
xavxyz committed Feb 23, 2017
1 parent e3d7954 commit f5875c3
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 103 deletions.
91 changes: 47 additions & 44 deletions main-client.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,48 @@
import './check-npm.js';

import { createNetworkInterface, createBatchingNetworkInterface } from 'apollo-client';
import 'isomorphic-fetch';
import { Accounts } from 'meteor/accounts-base';

import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore';
import { Accounts } from 'meteor/accounts-base';

import './check-npm.js';


// default network interface configuration object
const defaultNetworkInterfaceConfig = {
path: '/graphql', // default graphql server endpoint
opts: {}, // additional fetch options like `credentials` or `headers`
useMeteorAccounts: true, // if true, send an eventual Meteor login token to identify the current user with every request
batchingInterface: true, // use a BatchingNetworkInterface by default instead of a NetworkInterface
batchInterval: 10, // default batch interval
// default graphql server endpoint: ROOT_URL/graphql
// ex: http://locahost:3000/graphql, or https://www.my-app.com/graphql
uri: Meteor.absoluteUrl('graphql'),
// additional fetch options like `credentials` or `headers`
opts: {},
// enable the Meteor User Accounts middleware to identify the user with
// every request thanks to their login token
useMeteorAccounts: true,
// use a BatchingNetworkInterface by default instead of a NetworkInterface
batchingInterface: true,
// default batch interval
batchInterval: 10,
};

// create a pre-configured network interface
export const createMeteorNetworkInterface = (customNetworkInterfaceConfig = {}) => {
// create a new config object based on the default network interface config defined above
// and the custom network interface config passed to this function
// create a new config object based on the default network interface config
// defined above and the custom network interface config passed to this function
const config = {
...defaultNetworkInterfaceConfig,
...customNetworkInterfaceConfig,
};

// absoluteUrl adds a '/', so let's remove it first
let path = config.path;
if (path[0] === '/') {
path = path.slice(1);
}
// this will be true true if a BatchingNetworkInterface is meant to be used
// with a correct poll interval
const useBatchingInterface = config.batchingInterface && typeof config.batchInterval === 'number';

// allow the use of a batching network interface
const interfaceToUse = useBatchingInterface ? createBatchingNetworkInterface : createNetworkInterface;

// allow the use of a batching network interface; if the options.batchingInterface is not specified, fallback to the standard network interface
const interfaceToUse = config.batchingInterface ? createBatchingNetworkInterface : createNetworkInterface;

// default interface options
let interfaceOptions = {
uri: Meteor.absoluteUrl(path),
};

// if a BatchingNetworkInterface is used with a correct batch interval, add it to the options
if(config.batchingInterface && config.batchInterval) {
interfaceOptions.batchInterval = config.batchInterval;
}

// if 'fetch' has been configured to be called with specific opts, add it to the options
if(!_.isEmpty(config.opts)) {
interfaceOptions.opts = config.opts;
}

const networkInterface = interfaceToUse(interfaceOptions);
// configure the (batching?) network interface with the config defined above
const networkInterface = interfaceToUse(config);

// handle the creation of a Meteor User Accounts middleware
if (config.useMeteorAccounts) {
// possible cookie login token created by meteorhacks:fast-render
// and passed to the Apollo Client during server-side rendering
Expand All @@ -58,6 +53,7 @@ export const createMeteorNetworkInterface = (customNetworkInterfaceConfig = {})
// should this be handled somehow server-side?
console.error('[Meteor Apollo Integration] The current user is not handled with your GraphQL requests: you are trying to pass a login token to an Apollo Client instance defined client-side. This is only allowed during server-side rendering, please check your implementation.');
} else {
// add a middleware handling the current user to the network interface
networkInterface.use([{
applyMiddleware(request, next) {

Expand All @@ -67,45 +63,52 @@ export const createMeteorNetworkInterface = (customNetworkInterfaceConfig = {})

// define a current user token if existing
// ex: passed during server-side rendering or grabbed from local storage
let currentUserToken = localStorageLoginToken || loginToken;
const currentUserToken = localStorageLoginToken || loginToken;

// no token, meaning no user connected, just go to next possible middleware
if (!currentUserToken) {
next();
return;
}

// create the header object if needed.
if (!request.options.headers) {
// Create the header object if needed.
request.options.headers = new Headers();
}


// add the login token to the request headers
request.options.headers['meteor-login-token'] = currentUserToken;

// go to next middleware
next();
},
}]);
}
}


// return a configured network interface meant to be used by Apollo Client
return networkInterface;
};

// default Apollo Client config object
// default Apollo Client configuration object
const defaultClientConfig = {
// default network interface preconfigured
networkInterface: createMeteorNetworkInterface(),
// setup ssr mode if the client is configured server-side (ex: for SSR)
ssrMode: Meteor.isServer,
// leverage store normalization
dataIdFromObject: (result) => {
// store normalization with 'typename + Meteor's Mongo _id' if possible
if (result._id && result.__typename) {
// Store normalization with typename + Meteor's Mongo _id
const dataId = result.__typename + result._id;
return dataId;
}
// no store normalization
return null;
},
};

// create a new client config object based on the default Apollo Client config defined above
// and the client config passed to this function
// create a new client config object based on the default Apollo Client config
// defined above and the client config passed to this function
export const meteorClientConfig = (customClientConfig = {}) => ({
...defaultClientConfig,
...customClientConfig,
Expand Down
157 changes: 99 additions & 58 deletions main-server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import './check-npm.js';

import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
import bodyParser from 'body-parser';
import express from 'express';
Expand All @@ -8,91 +6,134 @@ import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { check } from 'meteor/check';
import { Accounts } from 'meteor/accounts-base';
import { _ } from 'meteor/underscore';

import './check-npm.js';

// import the configuration functions from the client so they can be used
// during server-side rendering for instance
export { createMeteorNetworkInterface, meteorClientConfig } from './main-client';

const defaultConfig = {
// default server configuration object
const defaultServerConfig = {
// graphql endpoint
path: '/graphql',
maxAccountsCacheSizeInMB: 1,
graphiql : Meteor.isDevelopment,
graphiqlPath : '/graphiql',
// additional Express server configuration (enable CORS there for instance)
configServer: graphQLServer => {},
// enable GraphiQL only in development mode
graphiql: Meteor.isDevelopment,
// GraphiQL endpoint
graphiqlPath: '/graphiql',
// GraphiQL options (default: log the current user in your request)
graphiqlOptions : {
passHeader : "'meteor-login-token': localStorage['Meteor.loginToken']"
},
configServer: (graphQLServer) => {},
};

const defaultOptions = {
// default graphql options to enhance the graphQLExpress server
const defaultGraphQLOptions = {
// ensure that a context object is defined for the resolvers
context: {},
// error formatting
formatError: e => ({
message: e.message,
locations: e.locations,
path: e.path
}),
// additional debug logging if execution errors occur in dev mode
debug: Meteor.isDevelopment,
};

if (Meteor.isDevelopment) {
defaultOptions.debug = true;
}

export const createApolloServer = (givenOptions = {}, givenConfig = {}) => {

let graphiqlOptions = Object.assign({}, defaultConfig.graphiqlOptions, givenConfig.graphiqlOptions);
let config = Object.assign({}, defaultConfig, givenConfig);
config.graphiqlOptions = graphiqlOptions;
export const createApolloServer = (customOptions = {}, customConfig = {}) => {

// create a new server config object based on the default server config
// defined above and the custom server config passed to this function
const config = {
...defaultServerConfig,
...customConfig,
};

// the Meteor GraphQL server is an Express server
const graphQLServer = express();

// enhance the GraphQL server with possible express middlewares
config.configServer(graphQLServer)

// GraphQL endpoint
// GraphQL endpoint, enhanced with JSON body parser
graphQLServer.use(config.path, bodyParser.json(), graphqlExpress(async (req) => {
let options,
user = null;

if (_.isFunction(givenOptions))
options = givenOptions(req);
else
options = givenOptions;

// Merge in the defaults
options = Object.assign({}, defaultOptions, options);
if (options.context) {
// don't mutate the context provided in options
options.context = Object.assign({}, options.context);
} else {
options.context = {};
}

// Get the token from the header
if (req.headers['meteor-login-token']) {
const token = req.headers['meteor-login-token'];
check(token, String);
const hashedToken = Accounts._hashLoginToken(token);

// Get the user from the database
user = await Meteor.users.findOne(
{"services.resume.loginTokens.hashedToken": hashedToken}
);

if (user) {
const loginToken = _.findWhere(user.services.resume.loginTokens, { hashedToken });
const expiresAt = Accounts._tokenExpiration(loginToken.when);
const isExpired = expiresAt < new Date();

if (!isExpired) {
options.context.userId = user._id;
options.context.user = user;
try {

// graphqlExpress can accept a function returning the option object
const customOptionsObject = typeof customOptions === 'function' ? customOptions(req) : customOptions;

// create a new apollo options object based on the default apollo options
// defined above and the custom apollo options passed to this function
const options = {
...defaultGraphQLOptions,
...customOptionsObject,
};

// get the login token from the headers request, given by the Meteor's
// network interface middleware if enabled
const loginToken = req.headers['meteor-login-token'];

// there is a possible current user connected!
if (loginToken) {
// throw an error if the token is not a string
check(loginToken, String);

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

// get the possible current user from the database
const currentUser = await Meteor.users.findOne(
{ "services.resume.loginTokens.hashedToken": hashedToken }
);

// the current user exists, add their information to the resolvers context
if (currentUser) {
// find the right login token corresponding, the current user may have
// several sessions logged on different browsers / computers
const tokenInformation = currentUser.services.resume.loginTokens.find(tokenInfo => tokenInfo.hashedToken === hashedToken);

// get an exploitable token expiration date
const expiresAt = Accounts._tokenExpiration(tokenInformation.when);

// true if the token is expired
const isExpired = expiresAt < new Date();

// if the token is still valid, give access to the current user
// information in the resolvers context
if (!isExpired) {
options.context = {
...options.context,
user: currentUser,
userId: currentUser._id,
};
}
}
}

// return the configured options to be used by the graphql server
return options;
} catch(error) {
// something went bad when configuring the graphql server, we do not
// swallow the error and display it in the server-side logs
console.error('[Meteor Apollo Integration] Something bad happened when handling a request on the GraphQL server. Your GraphQL server is not working as expected:', error);

// return the default graphql options anyway
return defaultGraphQLOptions;
}

return options;
}));

// Start GraphiQL if enabled
if (config.graphiql) {
graphQLServer.use(config.graphiqlPath, graphiqlExpress(_.extend(config.graphiqlOptions, {endpointURL : config.path})));
// GraphiQL endpoint
graphQLServer.use(config.graphiqlPath, graphiqlExpress({
// GraphiQL options
...config.graphiqlOptions,
// endpoint of the graphql server where to send requests
endpointURL: config.path,
}));
}

// This binds the specified paths to the Express server running Apollo + GraphiQL
Expand Down
1 change: 0 additions & 1 deletion package.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Package.describe({
Package.onUse(function(api) {
api.versionsFrom('1.4.0.1');
api.use(['ecmascript',
'underscore',
'accounts-base',
'tmeasday:check-npm-versions@0.3.1']);

Expand Down

0 comments on commit f5875c3

Please sign in to comment.