This commit is contained in:
SachaG 2019-01-30 10:42:17 +09:00
parent 6d1e8da40b
commit a13327340c
48 changed files with 492 additions and 801 deletions

View file

@ -8,6 +8,7 @@
"start": "meteor --settings settings.json",
"visualizer": "meteor --extra-packages bundle-visualizer --production --settings settings.json",
"lint": "eslint --cache --ext .jsx,js packages",
"lintfix": "eslint --cache --ext .jsx,js packages --fix",
"test-unit": "TEST_WATCH=1 meteor test-packages ./packages/* --port 3002 --driver-package meteortesting:mocha --raw-logs",
"test": "npm run test-unit",
"prettier": "node ./.vulcan/prettier/index.js write-changed",

View file

@ -3,11 +3,11 @@
*/
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import { runCallbacks } from '../../modules'
import { runCallbacks } from '../../modules';
import { Components } from 'meteor/vulcan:lib'
import { Components } from 'meteor/vulcan:lib';
import { CookiesProvider } from 'react-cookie';
import { BrowserRouter } from 'react-router-dom'
import { BrowserRouter } from 'react-router-dom';
const AppGenerator = ({ apolloClient }) => {
const App = (

View file

@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { onPageLoad } from 'meteor/server-render';
import AppGenerator from './components/AppGenerator'
import AppGenerator from './components/AppGenerator';
import {
createApolloClient,

View file

@ -1,4 +1,4 @@
import {addCallback, getActions} from 'meteor/vulcan:lib';
import { addCallback, getActions } from 'meteor/vulcan:lib';
/*

View file

@ -42,7 +42,7 @@ const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => {
return React.createElement(layout, layoutProps, <ErrorCatcher>{React.createElement(component, childComponentProps)}</ErrorCatcher>);
}}
/>
)};
);};
class App extends PureComponent {
constructor(props) {

View file

@ -6,11 +6,8 @@ HoC that provides access to flash messages stored in Redux state and actions to
of the apollo-link-state mutation patterns
*/
import {
registerStateLinkMutation,
registerStateLinkDefault,
} from 'meteor/vulcan:lib';
import {graphql, compose} from 'react-apollo';
import { registerStateLinkMutation, registerStateLinkDefault } from 'meteor/vulcan:lib';
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
// 1. Define the queries
// the @client tag tells graphQL that we fetch data from the cache
@ -53,13 +50,12 @@ registerStateLinkMutation({
name: 'flashMessagesFlash',
mutation: (obj, args, context, info) => {
// get relevant values from args
const {cache} = context;
const {content} = args;
const { cache } = context;
const { content } = args;
// retrieve current state
const currentFlashMessages = cache.readData({query: getMessagesQuery});
const currentFlashMessages = cache.readData({ query: getMessagesQuery });
// transform content
const flashType =
content && typeof content.type !== 'undefined' ? content.type : 'error';
const flashType = content && typeof content.type !== 'undefined' ? content.type : 'error';
const _id = currentFlashMessages.length;
const flashMessage = {
_id,
@ -75,50 +71,50 @@ registerStateLinkMutation({
const data = {
flashMessages: [...currentFlashMessages, flashMessage],
};
cache.writeData({data});
cache.writeData({ data });
return null;
},
});
registerStateLinkMutation({
name: 'flashMessagesMarkAsSeen',
mutation: (obj, args, context) => {
const {cache} = context;
const {i} = args;
const currentFlashMessages = cache.readData({query: getMessagesQuery});
currentFlashMessages[i] = {...currentFlashMessages[i], seen: true};
const { cache } = context;
const { i } = args;
const currentFlashMessages = cache.readData({ query: getMessagesQuery });
currentFlashMessages[i] = { ...currentFlashMessages[i], seen: true };
const data = {
flashMessages: currentFlashMessages,
};
cache.writeData({data});
cache.writeData({ data });
return null;
},
});
registerStateLinkMutation({
name: 'flashMessagesClear',
mutation: (obj, args, context) => {
const {cache} = context;
const {i} = args;
const currentFlashMessages = cache.readData({query: getMessagesQuery});
currentFlashMessages[i] = {...currentFlashMessages[i], show: false};
const { cache } = context;
const { i } = args;
const currentFlashMessages = cache.readData({ query: getMessagesQuery });
currentFlashMessages[i] = { ...currentFlashMessages[i], show: false };
const data = {
flashMessages: currentFlashMessages,
};
cache.writeData({data});
cache.writeData({ data });
return null;
},
});
registerStateLinkMutation({
name: 'flashMessagesClearSeen',
mutation: (obj, args, context) => {
const {cache} = context;
const currentFlashMessages = cache.readData({query: getMessagesQuery});
const { cache } = context;
const currentFlashMessages = cache.readData({ query: getMessagesQuery });
const newValue = currentFlashMessages.map(message =>
message.seen ? {...message, show: false} : message
message.seen ? { ...message, show: false } : message
);
const data = {
flashMessages: newValue,
};
cache.writeData({data});
cache.writeData({ data });
return null;
},
});
@ -140,9 +136,9 @@ const withMessages = compose(
// equivalent to mapStateToProps (map the graphql query to the component props)
graphql(getMessagesQuery, {
props: ({ownProps, data /*: { flashMessages }*/}) => {
const {flashMessages} = data;
return {...ownProps, messages: flashMessages};
props: ({ ownProps, data /*: { flashMessages }*/ }) => {
const { flashMessages } = data;
return { ...ownProps, messages: flashMessages };
},
})
);

View file

@ -34,7 +34,7 @@ Terms object can have the following properties:
*/
import {withApollo, graphql} from 'react-apollo';
import { withApollo, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'immutability-helper';
import {
@ -63,15 +63,15 @@ export default function withMulti(options) {
// see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855
pollInterval = typeof window === 'undefined' ? null : pollInterval;
const {collectionName, collection} = extractCollectionInfo(options);
const {fragmentName, fragment} = extractFragmentInfo(options, collectionName);
const { collectionName, collection } = extractCollectionInfo(options);
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
const typeName = collection.options.typeName;
const resolverName = collection.options.multiResolverName;
// build graphql query from options
const query = gql`
${multiClientTemplate({typeName, fragmentName, extraQueries})}
${multiClientTemplate({ typeName, fragmentName, extraQueries })}
${fragment}
`;
@ -99,9 +99,9 @@ export default function withMulti(options) {
alias: `with${Utils.pluralize(typeName)}`,
// graphql query options
options({terms, paginationTerms, client: apolloClient, currentUser}) {
options({ terms, paginationTerms, client: apolloClient, currentUser }) {
// get terms from options, then props, then pagination
const mergedTerms = {...options.terms, ...terms, ...paginationTerms};
const mergedTerms = { ...options.terms, ...terms, ...paginationTerms };
const graphQLOptions = {
variables: {
@ -121,8 +121,7 @@ export default function withMulti(options) {
// set to true if running into https://github.com/apollographql/apollo-client/issues/1186
if (options.notifyOnNetworkStatusChange) {
graphQLOptions.notifyOnNetworkStatusChange =
options.notifyOnNetworkStatusChange;
graphQLOptions.notifyOnNetworkStatusChange = options.notifyOnNetworkStatusChange;
}
return graphQLOptions;
@ -133,10 +132,8 @@ export default function withMulti(options) {
// see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts
const refetch = props.data.refetch,
// results = Utils.convertDates(collection, props.data[listResolverName]),
results =
props.data[resolverName] && props.data[resolverName].results,
totalCount =
props.data[resolverName] && props.data[resolverName].totalCount,
results = props.data[resolverName] && props.data[resolverName].results,
totalCount = props.data[resolverName] && props.data[resolverName].totalCount,
networkStatus = props.data.networkStatus,
loadingInitial = props.data.networkStatus === 1,
loading = props.data.networkStatus === 1,
@ -168,11 +165,8 @@ export default function withMulti(options) {
const newTerms =
typeof providedTerms === 'undefined'
? {
/*...props.ownProps.terms,*/ ...props.ownProps
.paginationTerms,
limit:
results.length +
props.ownProps.paginationTerms.itemsPerPage,
/*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms,
limit: results.length + props.ownProps.paginationTerms.itemsPerPage,
}
: providedTerms;
@ -193,8 +187,8 @@ export default function withMulti(options) {
: providedTerms;
return props.data.fetchMore({
variables: {input: {terms: newTerms}}, // ??? not sure about 'terms: newTerms'
updateQuery(previousResults, {fetchMoreResult}) {
variables: { input: { terms: newTerms } }, // ??? not sure about 'terms: newTerms'
updateQuery(previousResults, { fetchMoreResult }) {
// no more post to fetch
if (!fetchMoreResult.data) {
return previousResults;
@ -205,7 +199,7 @@ export default function withMulti(options) {
...fetchMoreResult.data[resolverName],
];
// return the previous results "augmented" with more
return {...previousResults, ...newResults};
return { ...previousResults, ...newResults };
},
});
},

View file

@ -25,7 +25,7 @@ import {
import Users from 'meteor/vulcan:users';
import isEmpty from 'lodash/isEmpty';
const defaultOptions = {create: true, update: true, upsert: true, delete: true};
const defaultOptions = { create: true, update: true, upsert: true, delete: true };
export function getDefaultMutations(options) {
let typeName, collectionName, mutationOptions;
@ -34,12 +34,12 @@ export function getDefaultMutations(options) {
// new single-argument API
typeName = arguments[0].typeName;
collectionName = arguments[0].collectionName || getCollectionName(typeName);
mutationOptions = {...defaultOptions, ...arguments[0].options};
mutationOptions = { ...defaultOptions, ...arguments[0].options };
} else {
// OpenCRUD backwards compatibility
collectionName = arguments[0];
typeName = getTypeName(collectionName);
mutationOptions = {...defaultOptions, ...arguments[1]};
mutationOptions = { ...defaultOptions, ...arguments[1] };
}
// register callbacks for documentation purposes
@ -72,7 +72,7 @@ export function getDefaultMutations(options) {
]);
},
async mutation(root, {data}, context) {
async mutation(root, { data }, context) {
const collection = context[collectionName];
// check if current user can pass check function; else throw error
@ -105,44 +105,34 @@ export function getDefaultMutations(options) {
*/
if (Meteor.isClient) {
registerWatchedMutation(
mutationName,
multiQueryName,
({mutation, query}) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const {selector, options} = parameters;
let results = query.result;
const document = mutation.result.data[mutationName].data;
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = mutation.result.data[mutationName].data;
if (belongsToSet(document, selector)) {
if (!isInSet(results[multiResolverName], document)) {
// make sure document hasn't been already added as this may be called several times
results[multiResolverName] = addToSet(
results[multiResolverName],
document
);
}
results[multiResolverName] = reorderSet(
results[multiResolverName],
options.sort
);
if (belongsToSet(document, selector)) {
if (!isInSet(results[multiResolverName], document)) {
// make sure document hasn't been already added as this may be called several times
results[multiResolverName] = addToSet(results[multiResolverName], document);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// create');
// console.log(mutation);
// console.log(query);
// console.log(collection);
// console.log(parameters);
// console.log(results);
return results;
results[multiResolverName] = reorderSet(results[multiResolverName], options.sort);
}
);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// create');
// console.log(mutation);
// console.log(query);
// console.log(collection);
// console.log(parameters);
// console.log(results);
return results;
});
}
}
@ -178,7 +168,7 @@ export function getDefaultMutations(options) {
]);
},
async mutation(root, {selector, data}, context) {
async mutation(root, { selector, data }, context) {
const collection = context[collectionName];
if (isEmpty(selector)) {
@ -190,9 +180,7 @@ export function getDefaultMutations(options) {
if (!document) {
throw new Error(
`Could not find document to update for selector: ${JSON.stringify(
selector
)}`
`Could not find document to update for selector: ${JSON.stringify(selector)}`
);
}
@ -229,57 +217,44 @@ export function getDefaultMutations(options) {
*/
if (Meteor.isClient) {
registerWatchedMutation(
mutationName,
multiQueryName,
({mutation, query}) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const {selector, options} = parameters;
let results = query.result;
const document = mutation.result.data[mutationName].data;
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = mutation.result.data[mutationName].data;
if (belongsToSet(document, selector)) {
// edited document belongs to the list
if (!isInSet(results[multiResolverName], document)) {
// if document wasn't already in list, add it
results[multiResolverName] = addToSet(
results[multiResolverName],
document
);
} else {
// if document was already in the list, update it
results[multiResolverName] = updateInSet(
results[multiResolverName],
document
);
}
results[multiResolverName] = reorderSet(
results[multiResolverName],
options.sort,
selector
);
if (belongsToSet(document, selector)) {
// edited document belongs to the list
if (!isInSet(results[multiResolverName], document)) {
// if document wasn't already in list, add it
results[multiResolverName] = addToSet(results[multiResolverName], document);
} else {
// if edited doesn't belong to current list anymore (based on view selector), remove it
results[multiResolverName] = removeFromSet(
results[multiResolverName],
document
);
// if document was already in the list, update it
results[multiResolverName] = updateInSet(results[multiResolverName], document);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// update');
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
results[multiResolverName] = reorderSet(
results[multiResolverName],
options.sort,
selector
);
} else {
// if edited doesn't belong to current list anymore (based on view selector), remove it
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
}
);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// update');
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
}
if (mutationOptions.upsert) {
@ -287,26 +262,22 @@ export function getDefaultMutations(options) {
mutations.upsert = {
description: `Mutation for upserting a ${typeName} document`,
async mutation(root, {selector, data}, context) {
async mutation(root, { selector, data }, context) {
const collection = context[collectionName];
// check if document exists already
const existingDocument = await Connectors.get(collection, selector, {
fields: {_id: 1},
fields: { _id: 1 },
});
if (existingDocument) {
return await collection.options.mutations.update.mutation(
root,
{selector, data},
{ selector, data },
context
);
} else {
return await collection.options.mutations.create.mutation(
root,
{data},
context
);
return await collection.options.mutations.create.mutation(root, { data }, context);
}
},
};
@ -321,8 +292,7 @@ export function getDefaultMutations(options) {
check(user, document) {
// OpenCRUD backwards compatibility
const check =
mutationOptions.deleteCheck || mutationOptions.removeCheck;
const check = mutationOptions.deleteCheck || mutationOptions.removeCheck;
if (check) {
return check(user, document);
}
@ -340,7 +310,7 @@ export function getDefaultMutations(options) {
]);
},
async mutation(root, {selector}, context) {
async mutation(root, { selector }, context) {
const collection = context[collectionName];
if (isEmpty(selector)) {
@ -351,9 +321,7 @@ export function getDefaultMutations(options) {
if (!document) {
throw new Error(
`Could not find document to delete for selector: ${JSON.stringify(
selector
)}`
`Could not find document to delete for selector: ${JSON.stringify(selector)}`
);
}
@ -388,25 +356,18 @@ export function getDefaultMutations(options) {
*/
if (Meteor.isClient) {
registerWatchedMutation(
mutationName,
multiQueryName,
({mutation, query}) => {
let results = query.result;
const document = mutation.result.data[mutationName].data;
results[multiResolverName] = removeFromSet(
results[multiResolverName],
document
);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// delete')
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
}
);
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
let results = query.result;
const document = mutation.result.data[mutationName].data;
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// delete')
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
}
@ -424,7 +385,7 @@ const registerCollectionCallbacks = (typeName, options) => {
{ document: 'The document being inserted' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
{ context: 'The context of the mutation'},
{ context: 'The context of the mutation' },
],
runs: 'sync',
returns: 'document',
@ -433,33 +394,32 @@ const registerCollectionCallbacks = (typeName, options) => {
});
registerCallback({
name: `${typeName}.create.before`,
iterator: {document: 'The document being inserted'},
properties: [{currentUser: 'The current user'}],
iterator: { document: 'The document being inserted' },
properties: [{ currentUser: 'The current user' }],
runs: 'sync',
returns: 'document',
description:
"Perform operations on a new document before it's inserted in the database.",
description: 'Perform operations on a new document before it\'s inserted in the database.',
});
registerCallback({
name: `${typeName}.create.after`,
iterator: {document: 'The document being inserted'},
properties: [{currentUser: 'The current user'}],
iterator: { document: 'The document being inserted' },
properties: [{ currentUser: 'The current user' }],
runs: 'sync',
returns: 'document',
description:
"Perform operations on a new document after it's inserted in the database but *before* the mutation returns it.",
'Perform operations on a new document after it\'s inserted in the database but *before* the mutation returns it.',
});
registerCallback({
name: `${typeName}.create.async`,
iterator: {document: 'The document being inserted'},
iterator: { document: 'The document being inserted' },
properties: [
{currentUser: 'The current user'},
{collection: 'The collection the document belongs to'},
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
],
runs: 'async',
returns: null,
description:
"Perform operations on a new document after it's inserted in the database asynchronously.",
'Perform operations on a new document after it\'s inserted in the database asynchronously.',
});
}
if (options.update) {
@ -471,7 +431,7 @@ const registerCollectionCallbacks = (typeName, options) => {
{ data: 'The client data' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
{ context: 'The context of the mutation'},
{ context: 'The context of the mutation' },
],
runs: 'sync',
returns: 'modifier',
@ -480,40 +440,33 @@ const registerCollectionCallbacks = (typeName, options) => {
});
registerCallback({
name: `${typeName}.update.before`,
iterator: {data: 'The client data'},
properties: [
{document: 'The document being edited'},
{currentUser: 'The current user'},
],
iterator: { data: 'The client data' },
properties: [{ document: 'The document being edited' }, { currentUser: 'The current user' }],
runs: 'sync',
returns: 'modifier',
description:
"Perform operations on a document before it's updated in the database.",
description: 'Perform operations on a document before it\'s updated in the database.',
});
registerCallback({
name: `${typeName}.update.after`,
iterator: {newDocument: 'The document after the update'},
properties: [
{document: 'The document being edited'},
{currentUser: 'The current user'},
],
iterator: { newDocument: 'The document after the update' },
properties: [{ document: 'The document being edited' }, { currentUser: 'The current user' }],
runs: 'sync',
returns: 'document',
description:
"Perform operations on a document after it's updated in the database but *before* the mutation returns it.",
'Perform operations on a document after it\'s updated in the database but *before* the mutation returns it.',
});
registerCallback({
name: `${typeName}.update.async`,
iterator: {newDocument: 'The document after the edit'},
iterator: { newDocument: 'The document after the edit' },
properties: [
{document: 'The document before the edit'},
{currentUser: 'The current user'},
{collection: 'The collection the document belongs to'},
{ document: 'The document before the edit' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
],
runs: 'async',
returns: null,
description:
"Perform operations on a document after it's updated in the database asynchronously.",
'Perform operations on a document after it\'s updated in the database asynchronously.',
});
}
if (options.delete) {
@ -523,8 +476,8 @@ const registerCollectionCallbacks = (typeName, options) => {
properties: [
{ currentUser: 'The current user' },
{ document: 'The document being removed' },
{ collection: 'The collection the document belongs to'},
{ context: 'The context of this mutation'}
{ collection: 'The collection the document belongs to' },
{ context: 'The context of this mutation' },
],
runs: 'sync',
returns: 'document',
@ -533,24 +486,23 @@ const registerCollectionCallbacks = (typeName, options) => {
});
registerCallback({
name: `${typeName}.delete.before`,
iterator: {document: 'The document being removed'},
properties: [{currentUser: 'The current user'}],
iterator: { document: 'The document being removed' },
properties: [{ currentUser: 'The current user' }],
runs: 'sync',
returns: null,
description:
"Perform operations on a document before it's removed from the database.",
description: 'Perform operations on a document before it\'s removed from the database.',
});
registerCallback({
name: `${typeName}.delete.async`,
properties: [
{document: 'The document being removed'},
{currentUser: 'The current user'},
{collection: 'The collection the document belongs to'},
{ document: 'The document being removed' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
],
runs: 'async',
returns: null,
description:
"Perform operations on a document after it's removed from the database asynchronously.",
'Perform operations on a document after it\'s removed from the database asynchronously.',
});
}
};

View file

@ -26,12 +26,12 @@ export function getDefaultResolvers(options) {
// new single-argument API
typeName = arguments[0].typeName;
collectionName = arguments[0].collectionName || getCollectionName(typeName);
resolverOptions = {...defaultOptions, ...arguments[0].options};
resolverOptions = { ...defaultOptions, ...arguments[0].options };
} else {
// OpenCRUD backwards compatibility
collectionName = arguments[0];
typeName = getTypeName(collectionName);
resolverOptions = {...defaultOptions, ...arguments[1]};
resolverOptions = { ...defaultOptions, ...arguments[1] };
}
return {
@ -40,8 +40,8 @@ export function getDefaultResolvers(options) {
multi: {
description: `A list of ${typeName} documents matching a set of query terms`,
async resolver(root, {input = {}}, context, {cacheControl}) {
const {terms = {}, enableCache = false, enableTotal = true} = input;
async resolver(root, { input = {} }, context, { cacheControl }) {
const { terms = {}, enableCache = false, enableTotal = true } = input;
debug('');
debugGroup(
@ -51,27 +51,22 @@ export function getDefaultResolvers(options) {
debug(`Terms: ${JSON.stringify(terms)}`);
if (cacheControl && enableCache) {
const maxAge =
resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({maxAge});
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({ maxAge });
}
// get currentUser and Users collection from context
const {currentUser, Users} = context;
const { currentUser, Users } = context;
// get collection based on collectionName argument
const collection = context[collectionName];
// get selector and options from terms and perform Mongo query
let {selector, options} = await collection.getParameters(
terms,
{},
context
);
let { selector, options } = await collection.getParameters(terms, {}, context);
options.skip = terms.offset;
debug({selector, options});
debug({ selector, options });
const docs = await Connectors.find(collection, selector, options);
@ -81,23 +76,17 @@ export function getDefaultResolvers(options) {
: docs;
// take the remaining documents and remove any fields that shouldn't be accessible
const restrictedDocs = Users.restrictViewableFields(
currentUser,
collection,
viewableDocs
);
const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);
// prime the cache
restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));
debug(`\x1b[33m=> ${restrictedDocs.length} documents returned\x1b[0m`);
debugGroupEnd();
debug(
`--------------- end \x1b[35m${typeName} Multi Resolver\x1b[0m ---------------`
);
debug(`--------------- end \x1b[35m${typeName} Multi Resolver\x1b[0m ---------------`);
debug('');
const data = {results: restrictedDocs};
const data = { results: restrictedDocs };
if (enableTotal) {
// get total count of documents matching the selector
@ -114,8 +103,8 @@ export function getDefaultResolvers(options) {
single: {
description: `A single ${typeName} document fetched by ID or slug`,
async resolver(root, {input = {}}, context, {cacheControl}) {
const {selector = {}, enableCache = false, allowNull = false} = input;
async resolver(root, { input = {} }, context, { cacheControl }) {
const { selector = {}, enableCache = false, allowNull = false } = input;
debug('');
debugGroup(
@ -125,12 +114,11 @@ export function getDefaultResolvers(options) {
debug(`Selector: ${JSON.stringify(selector)}`);
if (cacheControl && enableCache) {
const maxAge =
resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({maxAge});
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({ maxAge });
}
const {currentUser, Users} = context;
const { currentUser, Users } = context;
const collection = context[collectionName];
// use Dataloader if doc is selected by documentId/_id
@ -141,11 +129,11 @@ export function getDefaultResolvers(options) {
if (!doc) {
if (allowNull) {
return {result: null};
return { result: null };
} else {
throwError({
id: 'app.missing_document',
data: {documentId, selector},
data: { documentId, selector },
});
}
}
@ -164,20 +152,14 @@ export function getDefaultResolvers(options) {
);
}
const restrictedDoc = Users.restrictViewableFields(
currentUser,
collection,
doc
);
const restrictedDoc = Users.restrictViewableFields(currentUser, collection, doc);
debugGroupEnd();
debug(
`--------------- end \x1b[35m${typeName} Single Resolver\x1b[0m ---------------`
);
debug(`--------------- end \x1b[35m${typeName} Single Resolver\x1b[0m ---------------`);
debug('');
// filter out disallowed properties and return resulting document
return {result: restrictedDoc};
return { result: restrictedDoc };
},
},
};

View file

@ -39,11 +39,11 @@ export { default as withMutation } from './containers/withMutation.js';
export { default as withUpsert } from './containers/withUpsert.js';
export { default as withSiteData } from './containers/withSiteData.js';
export {default as withComponents} from './containers/withComponents';
export { default as withComponents } from './containers/withComponents';
// OpenCRUD backwards compatibility
export {default as withNew} from './containers/withCreate.js';
export {default as withEdit} from './containers/withUpdate.js';
export {default as withRemove} from './containers/withDelete.js';
export {default as withList} from './containers/withMulti.js';
export {default as withDocument} from './containers/withSingle.js';
export { default as withNew } from './containers/withCreate.js';
export { default as withEdit } from './containers/withUpdate.js';
export { default as withRemove } from './containers/withDelete.js';
export { default as withList } from './containers/withMulti.js';
export { default as withDocument } from './containers/withSingle.js';

View file

@ -2,10 +2,10 @@
import 'jsdom-global/register';
import React from 'react';
import expect from 'expect';
import {shallow} from 'enzyme';
import {Components} from 'meteor/vulcan:core';
import {initComponentTest} from 'meteor/vulcan:test';
import {withComponents} from '../lib/modules';
import { shallow } from 'enzyme';
import { Components } from 'meteor/vulcan:core';
import { initComponentTest } from 'meteor/vulcan:test';
import { withComponents } from '../lib/modules';
// we must import all the other components, so that "registerComponent" is called
import '../lib/modules';
@ -18,60 +18,51 @@ describe('vulcan-core/containers', function() {
// replace any component for testing purpose
const firstComponentName = Components[Object.keys(Components)[0]];
const FooComponent = () => 'FOO';
const components = {[firstComponentName]: FooComponent};
const MyComponent = withComponents(({Components}) =>
Components[firstComponentName]()
);
const components = { [firstComponentName]: FooComponent };
const MyComponent = withComponents(({ Components }) => Components[firstComponentName]());
const wrapper = shallow(<MyComponent components={components} />);
expect(wrapper.prop('Components')).toBeDefined();
expect(wrapper.prop('Components')[firstComponentName]).toEqual(
FooComponent
);
expect(wrapper.prop('Components')[firstComponentName]).toEqual(FooComponent);
expect(wrapper.html()).toEqual('FOO');
});
});
describe('handleOptions', function() {
const expectedCollectionName = 'COLLECTION_NAME';
const collectionNameOptions = {collectionName: expectedCollectionName};
const expectedCollection = {options: collectionNameOptions};
const collectionNameOptions = { collectionName: expectedCollectionName };
const expectedCollection = { options: collectionNameOptions };
it('get collectionName from collection', function() {
const options = {collection: expectedCollection};
const {collection, collectionName} = extractCollectionInfo(options);
const options = { collection: expectedCollection };
const { collection, collectionName } = extractCollectionInfo(options);
expect(collection).toEqual(expectedCollection);
expect(collectionName).toEqual(expectedCollectionName);
});
it('get collection from collectioName', function() {
// MOCK getCollection
const {collection, collectionName} = extractCollectionInfo(
collectionNameOptions
);
const { collection, collectionName } = extractCollectionInfo(collectionNameOptions);
expect(collection).toEqual(expectedCollection);
expect(collectionName).toEqual(expectedCollectionName);
});
const expectedFragmentName = 'FRAGMENT_NAME';
const expectedFragment = {
definitions: [{name: {value: expectedFragmentName}}],
definitions: [{ name: { value: expectedFragmentName } }],
};
it('get fragment from fragmentName', function() {
// MOCK getCollection
const options = {fragmentName: expectedFragmentName};
const {fragment, fragmentName} = extractFragmentInfo(options);
const options = { fragmentName: expectedFragmentName };
const { fragment, fragmentName } = extractFragmentInfo(options);
expect(fragment).toEqual(expectedFragment);
expect(fragmentName).toEqual(expectedFragmentName);
});
it('get fragmentName from fragment', function() {
const options = {fragment: expectedFragment};
const {fragment, fragmentName} = extractFragmentInfo(options);
const options = { fragment: expectedFragment };
const { fragment, fragmentName } = extractFragmentInfo(options);
expect(fragment).toEqual(expectedFragment);
expect(fragmentName).toEqual(expectedFragmentName);
});
it('get fragmentName and fragment from collectionName', function() {
// if options does not contain fragment, we get the collection default fragment based on its name
const options = {};
const {fragment, fragmentName} = extractFragmentInfo(
options,
expectedCollectionName
);
const { fragment, fragmentName } = extractFragmentInfo(options, expectedCollectionName);
expect(fragment).toEqual(expectedFragment);
expect(fragmentName).toEqual(expectedFragmentName);
});

View file

@ -1,9 +1,5 @@
import {addTrackFunction} from 'meteor/vulcan:events';
import {
getApolloClient,
getFragment,
createClientTemplate,
} from 'meteor/vulcan:lib';
import { addTrackFunction } from 'meteor/vulcan:events';
import { getApolloClient, getFragment, createClientTemplate } from 'meteor/vulcan:lib';
import gql from 'graphql-tag';
function trackInternal(eventName, eventProperties) {
@ -13,7 +9,7 @@ function trackInternal(eventName, eventProperties) {
const fragment = getFragment(fragmentName);
const mutation = gql`
${createClientTemplate({typeName: 'AnalyticsEvent', fragmentName})}
${createClientTemplate({ typeName: 'AnalyticsEvent', fragmentName })}
${fragment}
`;
@ -23,7 +19,7 @@ function trackInternal(eventName, eventProperties) {
properties: eventProperties,
},
};
apolloClient.mutate({mutation, variables});
apolloClient.mutate({ mutation, variables });
}
addTrackFunction(trackInternal);

View file

@ -1,23 +1,18 @@
import {ApolloClient} from 'apollo-client';
import {ApolloLink} from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import watchedMutationLink from './links/watchedMutation';
import httpLink from './links/http';
import meteorAccountsLink from './links/meteor';
import errorLink from './links/error';
import {createStateLink} from '../../modules/apollo-common';
import { createStateLink } from '../../modules/apollo-common';
import cache from './cache';
// these links do not change once created
const staticLinks = [
watchedMutationLink,
errorLink,
meteorAccountsLink,
httpLink,
];
const staticLinks = [watchedMutationLink, errorLink, meteorAccountsLink, httpLink];
let apolloClient;
export const createApolloClient = () => {
const stateLink = createStateLink({cache});
const stateLink = createStateLink({ cache });
const newClient = new ApolloClient({
link: ApolloLink.from([stateLink, ...staticLinks]),
cache,

View file

@ -1,4 +1,4 @@
import {InMemoryCache} from 'apollo-cache-inmemory';
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache()
//ssr

View file

@ -1,11 +1,9 @@
import {onError} from 'apollo-link-error';
import { onError } from 'apollo-link-error';
const errorLink = onError(({graphQLErrors, networkError}) => {
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({message, locations, path}) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
graphQLErrors.map(({ message, locations, path }) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
});

View file

@ -1,4 +1,4 @@
import {HttpLink} from 'apollo-link-http';
import { HttpLink } from 'apollo-link-http';
const httpLink = new HttpLink({
uri: '/graphql',

View file

@ -1,4 +1,4 @@
import {MeteorAccountsLink} from 'meteor/apollo';
import { MeteorAccountsLink } from 'meteor/apollo';
const meteorAccountsLink = new MeteorAccountsLink();
export default meteorAccountsLink;

View file

@ -4,7 +4,7 @@
* @see https://github.com/haytko/apollo-link-watched-mutation
*/
import WatchedMutationLink from 'apollo-link-watched-mutation';
import {WatchedMutations} from '../updates';
import { WatchedMutations } from '../updates';
import cache from '../cache';
const watchedMutationLink = new WatchedMutationLink(cache, WatchedMutations);

View file

@ -5,11 +5,7 @@ import Mingo from 'mingo';
export const WatchedMutations = {};
export const registerWatchedMutation = (
mutationName,
queryName,
updateFunction
) => {
export const registerWatchedMutation = (mutationName, queryName, updateFunction) => {
WatchedMutations[mutationName] = {
[queryName]: updateFunction,
};
@ -30,8 +26,7 @@ export const belongsToSet = (document, selector) => {
Test if a document is already in a result set
*/
export const isInSet = (data, document) =>
data.results.find(item => item._id === document._id);
export const isInSet = (data, document) => data.results.find(item => item._id === document._id);
/*
@ -53,9 +48,9 @@ Update a document in a set of results
*/
export const updateInSet = (queryData, document) => {
const oldDocument = queryData.results.find(item => item._id === document._id);
const newDocument = {...oldDocument, ...document};
const newDocument = { ...oldDocument, ...document };
const index = queryData.results.findIndex(item => item._id === document._id);
const newData = {results: [...queryData.results]}; // clone
const newData = { results: [...queryData.results] }; // clone
newData.results[index] = newDocument;
return newData;
};

View file

@ -7,7 +7,7 @@
*/
import Cookies from 'universal-cookie';
import {Meteor} from 'meteor/meteor';
import { Meteor } from 'meteor/meteor';
const cookie = new Cookies();
@ -26,9 +26,7 @@ function setToken(loginToken, expires) {
function initToken() {
const loginToken = global.localStorage['Meteor.loginToken'];
const loginTokenExpires = new Date(
global.localStorage['Meteor.loginTokenExpires']
);
const loginTokenExpires = new Date(global.localStorage['Meteor.loginTokenExpires']);
if (loginToken) {
setToken(loginToken, loginTokenExpires);

View file

@ -11,17 +11,12 @@
* Example
* @see https://hackernoon.com/storing-local-state-in-react-with-apollo-link-state-738f6ca45569
*/
import {withClientState} from 'apollo-link-state';
import { withClientState } from 'apollo-link-state';
/**
* Create a state link
*/
export const createStateLink = ({
cache,
resolvers,
defaults,
...otherOptions
}) => {
export const createStateLink = ({ cache, resolvers, defaults, ...otherOptions }) => {
const stateLink = withClientState({
cache,
defaults: defaults || getStateLinkDefaults(),
@ -36,11 +31,7 @@ const registeredDefaults = {};
/**
* Defaults are default response to queries
*/
export const registerStateLinkDefault = ({
name,
defaultValue,
options = {},
}) => {
export const registerStateLinkDefault = ({ name, defaultValue, options = {} }) => {
registeredDefaults[name] = defaultValue;
return registeredDefaults;
};
@ -49,7 +40,7 @@ export const getStateLinkDefaults = () => registeredDefaults;
// Mutation are equivalent to a Redux Action + Reducer
// except it uses GraphQL to retrieve/update data in the cache
const registeredMutations = {};
export const registerStateLinkMutation = ({name, mutation, options = {}}) => {
export const registerStateLinkMutation = ({ name, mutation, options = {} }) => {
registeredMutations[name] = mutation;
return registeredMutations;
};

View file

@ -10,9 +10,9 @@ import deepmerge from 'deepmerge';
import GraphQLJSON from 'graphql-type-json';
import GraphQLDate from 'graphql-date';
import Vulcan from './config.js'; // used for global export
import {Utils} from './utils.js';
import {disableFragmentWarnings} from 'graphql-tag';
import {isIntlField} from './intl.js';
import { Utils } from './utils.js';
import { disableFragmentWarnings } from 'graphql-tag';
import { isIntlField } from './intl.js';
import {
selectorInputTemplate,
mainTypeTemplate,
@ -44,11 +44,7 @@ const getGraphQLType = (schema, fieldName, isInput = false) => {
const field = schema[fieldName];
const type = field.type.singleType;
const typeName =
typeof type === 'object'
? 'Object'
: typeof type === 'function'
? type.name
: type;
typeof type === 'object' ? 'Object' : typeof type === 'function' ? type.name : type;
if (field.isIntlData) {
return isInput ? '[IntlValueInput]' : '[IntlValue]';
@ -119,13 +115,13 @@ export const GraphQLSchema = {
// queries
queries: [],
addQuery(query, description) {
this.queries.push({query, description});
this.queries.push({ query, description });
},
// mutations
mutations: [],
addMutation(mutation, description) {
this.mutations.push({mutation, description});
this.mutations.push({ mutation, description });
},
// add resolvers
@ -182,9 +178,7 @@ export const GraphQLSchema = {
) {
const fieldDescription = field.description;
const fieldDirective = isIntlField(field) ? '@intl' : '';
const fieldArguments = isIntlField(field)
? [{name: 'locale', type: 'String'}]
: [];
const fieldArguments = isIntlField(field) ? [{ name: 'locale', type: 'String' }] : [];
// if field has a resolveAs, push it to schema
if (field.resolveAs) {
@ -208,13 +202,9 @@ export const GraphQLSchema = {
const resolver = {
[typeName]: {
[resolverName]: (document, args, context, info) => {
const {Users, currentUser} = context;
const { Users, currentUser } = context;
// check that current user has permission to access the original non-resolved field
const canReadField = Users.canReadField(
currentUser,
field,
document
);
const canReadField = Users.canReadField(currentUser, field, document);
return canReadField
? field.resolveAs.resolver(document, args, context, info)
: null;
@ -316,80 +306,61 @@ export const GraphQLSchema = {
const fields = this.getFields(schema, typeName);
const {interfaces = [], resolvers, mutations} = collection.options;
const { interfaces = [], resolvers, mutations } = collection.options;
const description = collection.options.description
? collection.options.description
: `Type for ${collectionName}`;
const {
mainType,
create,
update,
selector,
selectorUnique,
orderBy,
} = fields;
const { mainType, create, update, selector, selectorUnique, orderBy } = fields;
if (mainType.length) {
schemaFragments.push(
mainTypeTemplate({typeName, description, interfaces, fields: mainType})
mainTypeTemplate({ typeName, description, interfaces, fields: mainType })
);
schemaFragments.push(deleteInputTemplate({typeName}));
schemaFragments.push(singleInputTemplate({typeName}));
schemaFragments.push(multiInputTemplate({typeName}));
schemaFragments.push(singleOutputTemplate({typeName}));
schemaFragments.push(multiOutputTemplate({typeName}));
schemaFragments.push(mutationOutputTemplate({typeName}));
schemaFragments.push(deleteInputTemplate({ typeName }));
schemaFragments.push(singleInputTemplate({ typeName }));
schemaFragments.push(multiInputTemplate({ typeName }));
schemaFragments.push(singleOutputTemplate({ typeName }));
schemaFragments.push(multiOutputTemplate({ typeName }));
schemaFragments.push(mutationOutputTemplate({ typeName }));
if (create.length) {
schemaFragments.push(createInputTemplate({typeName}));
schemaFragments.push(
createDataInputTemplate({typeName, fields: create})
);
schemaFragments.push(createInputTemplate({ typeName }));
schemaFragments.push(createDataInputTemplate({ typeName, fields: create }));
}
if (update.length) {
schemaFragments.push(updateInputTemplate({typeName}));
schemaFragments.push(upsertInputTemplate({typeName}));
schemaFragments.push(
updateDataInputTemplate({typeName, fields: update})
);
schemaFragments.push(updateInputTemplate({ typeName }));
schemaFragments.push(upsertInputTemplate({ typeName }));
schemaFragments.push(updateDataInputTemplate({ typeName, fields: update }));
}
schemaFragments.push(selectorInputTemplate({typeName, fields: selector}));
schemaFragments.push(selectorInputTemplate({ typeName, fields: selector }));
schemaFragments.push(
selectorUniqueInputTemplate({typeName, fields: selectorUnique})
);
schemaFragments.push(selectorUniqueInputTemplate({ typeName, fields: selectorUnique }));
schemaFragments.push(orderByInputTemplate({typeName, fields: orderBy}));
schemaFragments.push(orderByInputTemplate({ typeName, fields: orderBy }));
if (!_.isEmpty(resolvers)) {
const queryResolvers = {};
// single
if (resolvers.single) {
addGraphQLQuery(
singleQueryTemplate({typeName}),
resolvers.single.description
addGraphQLQuery(singleQueryTemplate({ typeName }), resolvers.single.description);
queryResolvers[Utils.camelCaseify(typeName)] = resolvers.single.resolver.bind(
resolvers.single
);
queryResolvers[
Utils.camelCaseify(typeName)
] = resolvers.single.resolver.bind(resolvers.single);
}
// multi
if (resolvers.multi) {
addGraphQLQuery(
multiQueryTemplate({typeName}),
resolvers.multi.description
);
addGraphQLQuery(multiQueryTemplate({ typeName }), resolvers.multi.description);
queryResolvers[
Utils.camelCaseify(Utils.pluralize(typeName))
] = resolvers.multi.resolver.bind(resolvers.multi);
}
addGraphQLResolvers({Query: {...queryResolvers}});
addGraphQLResolvers({ Query: { ...queryResolvers } });
}
if (!_.isEmpty(mutations)) {
@ -403,13 +374,10 @@ export const GraphQLSchema = {
`// Warning: you defined a "create" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "create" mutation or define a "canCreate" property on a field to disable this warning`
);
} else {
addGraphQLMutation(
createMutationTemplate({typeName}),
mutations.create.description
addGraphQLMutation(createMutationTemplate({ typeName }), mutations.create.description);
mutationResolvers[`create${typeName}`] = mutations.create.mutation.bind(
mutations.create
);
mutationResolvers[
`create${typeName}`
] = mutations.create.mutation.bind(mutations.create);
}
}
// update
@ -421,13 +389,10 @@ export const GraphQLSchema = {
`// Warning: you defined an "update" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "update" mutation or define a "canUpdate" property on a field to disable this warning`
);
} else {
addGraphQLMutation(
updateMutationTemplate({typeName}),
mutations.update.description
addGraphQLMutation(updateMutationTemplate({ typeName }), mutations.update.description);
mutationResolvers[`update${typeName}`] = mutations.update.mutation.bind(
mutations.update
);
mutationResolvers[
`update${typeName}`
] = mutations.update.mutation.bind(mutations.update);
}
}
// upsert
@ -439,27 +404,19 @@ export const GraphQLSchema = {
`// Warning: you defined an "upsert" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "upsert" mutation or define a "canUpdate" property on a field to disable this warning`
);
} else {
addGraphQLMutation(
upsertMutationTemplate({typeName}),
mutations.upsert.description
addGraphQLMutation(upsertMutationTemplate({ typeName }), mutations.upsert.description);
mutationResolvers[`upsert${typeName}`] = mutations.upsert.mutation.bind(
mutations.upsert
);
mutationResolvers[
`upsert${typeName}`
] = mutations.upsert.mutation.bind(mutations.upsert);
}
}
// delete
if (mutations.delete) {
// e.g. "deleteMovie(input: DeleteMovieInput) : Movie"
addGraphQLMutation(
deleteMutationTemplate({typeName}),
mutations.delete.description
);
mutationResolvers[
`delete${typeName}`
] = mutations.delete.mutation.bind(mutations.delete);
addGraphQLMutation(deleteMutationTemplate({ typeName }), mutations.delete.description);
mutationResolvers[`delete${typeName}`] = mutations.delete.mutation.bind(mutations.delete);
}
addGraphQLResolvers({Mutation: {...mutationResolvers}});
addGraphQLResolvers({ Mutation: { ...mutationResolvers } });
}
graphQLSchema = schemaFragments.join('\n\n') + '\n\n\n';
} else {
@ -475,9 +432,7 @@ export const GraphQLSchema = {
// getters
getSchema() {
if (!(this.finalSchema && this.finalSchema.length)) {
throw new Error(
'Warning: trying to access schema before it has been created by the server.'
);
throw new Error('Warning: trying to access schema before it has been created by the server.');
}
return this.finalSchema[0];
},
@ -503,21 +458,11 @@ Vulcan.getGraphQLSchema = () => {
return schema;
};
export const addGraphQLCollection = GraphQLSchema.addCollection.bind(
GraphQLSchema
);
export const addGraphQLCollection = GraphQLSchema.addCollection.bind(GraphQLSchema);
export const addGraphQLSchema = GraphQLSchema.addSchema.bind(GraphQLSchema);
export const addGraphQLQuery = GraphQLSchema.addQuery.bind(GraphQLSchema);
export const addGraphQLMutation = GraphQLSchema.addMutation.bind(GraphQLSchema);
export const addGraphQLResolvers = GraphQLSchema.addResolvers.bind(
GraphQLSchema
);
export const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(
GraphQLSchema
);
export const addToGraphQLContext = GraphQLSchema.addToContext.bind(
GraphQLSchema
);
export const addGraphQLDirective = GraphQLSchema.addDirective.bind(
GraphQLSchema
);
export const addGraphQLResolvers = GraphQLSchema.addResolvers.bind(GraphQLSchema);
export const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(GraphQLSchema);
export const addToGraphQLContext = GraphQLSchema.addToContext.bind(GraphQLSchema);
export const addGraphQLDirective = GraphQLSchema.addDirective.bind(GraphQLSchema);

View file

@ -6,44 +6,37 @@
// Meteor WebApp use a Connect server, so we need to
// use apollo-server-express integration
//import express from 'express';
import {ApolloServer} from 'apollo-server-express';
import { ApolloServer } from 'apollo-server-express';
import {Meteor} from 'meteor/meteor';
import { Meteor } from 'meteor/meteor';
import {WebApp} from 'meteor/webapp';
import { WebApp } from 'meteor/webapp';
import bodyParser from 'body-parser';
// import cookiesMiddleware from 'universal-cookie-express';
// import Cookies from 'universal-cookie';
import voyagerMiddleware from 'graphql-voyager/middleware/express';
import getVoyagerConfig from './voyager';
import {graphiqlMiddleware, getGraphiqlConfig} from './graphiql';
import { graphiqlMiddleware, getGraphiqlConfig } from './graphiql';
import getPlaygroundConfig from './playground';
import initGraphQL from './initGraphQL';
import './settings';
import {engineConfig} from './engine';
import {initContext, computeContextFromReq} from './context.js';
import { engineConfig } from './engine';
import { initContext, computeContextFromReq } from './context.js';
import {GraphQLSchema} from '../../modules/graphql.js';
import { GraphQLSchema } from '../../modules/graphql.js';
import {enableSSR} from '../apollo-ssr';
import { enableSSR } from '../apollo-ssr';
import universalCookiesMiddleware from 'universal-cookie-express';
import {
getApolloApplyMiddlewareOptions,
getApolloServerOptions,
} from './settings';
import { getApolloApplyMiddlewareOptions, getApolloServerOptions } from './settings';
import {getSetting} from '../../modules/settings.js';
import {formatError} from 'apollo-errors';
import { getSetting } from '../../modules/settings.js';
import { formatError } from 'apollo-errors';
export const setupGraphQLMiddlewares = (
apolloServer,
config,
apolloApplyMiddlewareOptions
) => {
export const setupGraphQLMiddlewares = (apolloServer, config, apolloApplyMiddlewareOptions) => {
// IMPORTANT: order matters !
// 1 - Add request parsing middleware
// 2 - Add apollo specific middlewares
@ -59,12 +52,9 @@ export const setupGraphQLMiddlewares = (
// parse request (order matters)
WebApp.connectHandlers.use(
config.path,
bodyParser.json({limit: getSetting('apolloServer.jsonParserOptions.limit')})
);
WebApp.connectHandlers.use(
config.path,
bodyParser.text({type: 'application/graphql'})
bodyParser.json({ limit: getSetting('apolloServer.jsonParserOptions.limit') })
);
WebApp.connectHandlers.use(config.path, bodyParser.text({ type: 'application/graphql' }));
// Provide the Meteor WebApp Connect server instance to Apollo
// Apollo will use it instead of its own HTTP server when handling requests
@ -88,15 +78,9 @@ export const setupGraphQLMiddlewares = (
export const setupToolsMiddlewares = config => {
// Voyager is a GraphQL schema visual explorer
// available on /voyager as a default
WebApp.connectHandlers.use(
config.voyagerPath,
voyagerMiddleware(getVoyagerConfig(config))
);
WebApp.connectHandlers.use(config.voyagerPath, voyagerMiddleware(getVoyagerConfig(config)));
// Setup GraphiQL
WebApp.connectHandlers.use(
config.graphiqlPath,
graphiqlMiddleware(getGraphiqlConfig(config))
);
WebApp.connectHandlers.use(config.graphiqlPath, graphiqlMiddleware(getGraphiqlConfig(config)));
};
/**
@ -158,7 +142,7 @@ export const onStart = () => {
formatError,
tracing: getSetting('apolloTracing', Meteor.isDevelopment),
cacheControl: true,
context: ({req}) => context(req),
context: ({ req }) => context(req),
...getApolloServerOptions(),
},
});
@ -170,7 +154,7 @@ export const onStart = () => {
setupToolsMiddlewares(config);
}
// ssr
enableSSR({computeContext: context});
enableSSR({ computeContext: context });
};
// createApolloServer when server startup
Meteor.startup(onStart);

View file

@ -12,14 +12,14 @@
//import deepmerge from 'deepmerge';
import DataLoader from 'dataloader';
import {Collections} from '../../modules/collections.js';
import {runCallbacks} from '../../modules/callbacks.js';
import { Collections } from '../../modules/collections.js';
import { runCallbacks } from '../../modules/callbacks.js';
import findByIds from '../../modules/findbyids.js';
import {GraphQLSchema} from '../../modules/graphql.js';
import { GraphQLSchema } from '../../modules/graphql.js';
import _merge from 'lodash/merge';
import {getUser} from 'meteor/apollo';
import {getHeaderLocale} from '../intl.js';
import {getSetting} from '../../modules/settings.js';
import { getUser } from 'meteor/apollo';
import { getHeaderLocale } from '../intl.js';
import { getSetting } from '../../modules/settings.js';
/**
* Called once on server creation
@ -28,7 +28,7 @@ import {getSetting} from '../../modules/settings.js';
export const initContext = currentContext => {
let context;
if (currentContext) {
context = {...currentContext};
context = { ...currentContext };
} else {
context = {};
}
@ -52,10 +52,7 @@ import Cookies from 'universal-cookie';
// initial request will get the login token from a cookie, subsequent requests from
// the header
const getAuthToken = req => {
return (
req.headers.authorization ||
new Cookies(req.cookies).get('meteor_login_token')
);
return req.headers.authorization || new Cookies(req.cookies).get('meteor_login_token');
};
// @see https://www.apollographql.com/docs/react/recipes/meteor#Server
const setupAuthToken = async (context, req) => {
@ -75,8 +72,8 @@ export const computeContextFromReq = (currentContext, customContextFromReq) => {
// givenOptions can be either a function of the request or an object
const getBaseContext = req =>
customContextFromReq
? {...currentContext, ...customContextFromReq(req)}
: {...currentContext};
? { ...currentContext, ...customContextFromReq(req) }
: { ...currentContext };
// Previous implementation
// Now meteor/apollo already provide this
// Get the token from the header
@ -127,7 +124,7 @@ export const computeContextFromReq = (currentContext, customContextFromReq) => {
// console.log('// apollo_server.js locale:', req.headers.locale);
// if apiKey is present, assign "fake" currentUser with admin rights
if (headers.apikey && (headers.apikey === getSetting('vulcan.apiKey'))) {
if (headers.apikey && headers.apikey === getSetting('vulcan.apiKey')) {
context.currentUser = { isAdmin: true, isApiUser: true };
}

View file

@ -1,7 +1,6 @@
import {getSetting} from '../../modules/settings.js';
import { getSetting } from '../../modules/settings.js';
// see https://github.com/apollographql/apollo-cache-control
export const engineApiKey =
process.env.ENGINE_API_KEY || getSetting('apolloEngine.apiKey');
export const engineApiKey = process.env.ENGINE_API_KEY || getSetting('apolloEngine.apiKey');
// options now available:
// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#EngineReportingOptions
export const engineConfig = engineApiKey

View file

@ -42,18 +42,14 @@ function safeSerialize(data) {
export function renderGraphiQL(data) {
const endpointURL = data.endpointURL;
const endpointWs =
endpointURL.startsWith('ws://') || endpointURL.startsWith('wss://');
const endpointWs = endpointURL.startsWith('ws://') || endpointURL.startsWith('wss://');
const subscriptionsEndpoint = data.subscriptionsEndpoint;
const usingHttp = !endpointWs;
const usingWs = endpointWs || !!subscriptionsEndpoint;
const endpointURLWs =
usingWs && (endpointWs ? endpointURL : subscriptionsEndpoint);
const endpointURLWs = usingWs && (endpointWs ? endpointURL : subscriptionsEndpoint);
const queryString = data.query;
const variablesString = data.variables
? JSON.stringify(data.variables, null, 2)
: null;
const variablesString = data.variables ? JSON.stringify(data.variables, null, 2) : null;
const resultString = null;
const operationName = data.operationName;
const passHeader = data.passHeader ? data.passHeader : '';
@ -87,11 +83,7 @@ export function renderGraphiQL(data) {
? `<link href="//cdn.jsdelivr.net/npm/codemirror@5/theme/${editorTheme}.min.css" rel="stylesheet" />`
: ''
}
${
usingHttp
? '<script src="//cdn.jsdelivr.net/fetch/2.0.1/fetch.min.js"></script>'
: ''
}
${usingHttp ? '<script src="//cdn.jsdelivr.net/fetch/2.0.1/fetch.min.js"></script>' : ''}
${
usingWs
? `<script src="//unpkg.com/subscriptions-transport-ws@${SUBSCRIPTIONS_TRANSPORT_VERSION}/browser/client.js"></script>`
@ -263,8 +255,7 @@ function createGraphiQLData(params, options) {
endpointURL: options.endpointURL,
subscriptionsEndpoint: options.subscriptionsEndpoint,
query: params.query || options.query,
variables:
(params.variables && JSON.parse(params.variables)) || options.variables,
variables: (params.variables && JSON.parse(params.variables)) || options.variables,
operationName: params.operationName || options.operationName,
passHeader: options.passHeader,
editorTheme: options.editorTheme,

View file

@ -1,4 +1,4 @@
export * from './apollo_server2';
export * from './settings';
export {default as initGraphQL} from './initGraphQL';
export { default as initGraphQL } from './initGraphQL';

View file

@ -2,9 +2,9 @@
* Init the graphQL schema
*/
import {makeExecutableSchema} from 'apollo-server';
import {GraphQLSchema} from '../../modules/graphql.js';
import {runCallbacks} from '../../modules/callbacks.js';
import { makeExecutableSchema } from 'apollo-server';
import { GraphQLSchema } from '../../modules/graphql.js';
import { runCallbacks } from '../../modules/callbacks.js';
const getQueries = () =>
`type Query {

View file

@ -27,5 +27,4 @@ let apolloApplyMiddlewareOptions = {};
export const registerApolloApplyMiddlewareOptions = options => {
apolloApplyMiddlewareOptions = _merge(apolloApplyMiddlewareOptions, options);
};
export const getApolloApplyMiddlewareOptions = () =>
apolloApplyMiddlewareOptions;
export const getApolloApplyMiddlewareOptions = () => apolloApplyMiddlewareOptions;

View file

@ -5,29 +5,29 @@
* /!\ It must be recreated on every request
*/
import {ApolloClient} from 'apollo-client';
import {InMemoryCache} from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import {SchemaLink} from 'apollo-link-schema';
import {GraphQLSchema} from '../../modules/graphql.js';
import { SchemaLink } from 'apollo-link-schema';
import { GraphQLSchema } from '../../modules/graphql.js';
import {createStateLink} from '../../modules/apollo-common';
import {ApolloLink} from 'apollo-link';
import { createStateLink } from '../../modules/apollo-common';
import { ApolloLink } from 'apollo-link';
// @see https://www.apollographql.com/docs/react/features/server-side-rendering.html#local-queries
// import { createHttpLink } from 'apollo-link-http';
// import fetch from 'node-fetch'
export const createClient = async ({req, computeContext}) => {
export const createClient = async ({ req, computeContext }) => {
// init
// stateLink will init the client internal state
const cache = new InMemoryCache();
const stateLink = createStateLink({cache});
const stateLink = createStateLink({ cache });
// schemaLink will fetch data directly based on the executable schema
const schema = GraphQLSchema.getExecutableSchema();
// this is the resolver context
const context = await computeContext(req);
const schemaLink = new SchemaLink({schema, context});
const schemaLink = new SchemaLink({ schema, context });
const client = new ApolloClient({
ssrMode: true,
link: ApolloLink.from([stateLink, schemaLink]),

View file

@ -5,7 +5,7 @@ import React from 'react';
import { ApolloProvider } from 'react-apollo';
import { StaticRouter } from 'react-router';
import { Components } from 'meteor/vulcan:lib'
import { Components } from 'meteor/vulcan:lib';
import { CookiesProvider } from 'react-cookie';
@ -16,7 +16,7 @@ import Cookies from 'universal-cookie';
const AppGenerator = ({ req, apolloClient, context }) => {
// TODO: universalCookies should be defined here, but it isn't
// @see https://github.com/meteor/meteor-feature-requests/issues/174#issuecomment-441047495
const cookies = new Cookies(req.cookies) // req.universalCookies;
const cookies = new Cookies(req.cookies); // req.universalCookies;
const App = (
<ApolloProvider client={apolloClient}>
<StaticRouter location={req.url} context={context}>
@ -26,6 +26,6 @@ const AppGenerator = ({ req, apolloClient, context }) => {
</StaticRouter>
</ApolloProvider>
);
return App
return App;
};
export default AppGenerator;

View file

@ -2,25 +2,21 @@
* Actually enable SSR
*/
import {
populateComponentsApp,
populateRoutesApp,
initializeFragments,
} from 'meteor/vulcan:lib';
import { populateComponentsApp, populateRoutesApp, initializeFragments } from 'meteor/vulcan:lib';
// onPageLoad is mostly equivalent to an Express middleware
// excepts it is tailored to handle Meteor server side rendering
import {onPageLoad} from 'meteor/server-render';
import { onPageLoad } from 'meteor/server-render';
import makePageRenderer from './renderPage';
const enableSSR = ({computeContext}) => {
const enableSSR = ({ computeContext }) => {
Meteor.startup(() => {
// init the application components and routes, including components & routes from 3rd-party packages
initializeFragments();
populateComponentsApp();
populateRoutesApp();
// render the page
onPageLoad(makePageRenderer({computeContext}));
onPageLoad(makePageRenderer({ computeContext }));
});
};

View file

@ -1 +1 @@
export {default as enableSSR} from './enableSSR';
export { default as enableSSR } from './enableSSR';

View file

@ -6,22 +6,22 @@
*/
import React from 'react';
import ReactDOM from 'react-dom/server';
import {renderToStringWithData} from 'react-apollo';
import { renderToStringWithData } from 'react-apollo';
import {runCallbacks} from '../../modules/callbacks';
import {createClient} from './apolloClient';
import { runCallbacks } from '../../modules/callbacks';
import { createClient } from './apolloClient';
import Head from './components/Head';
import ApolloState from './components/ApolloState';
import AppGenerator from './components/AppGenerator';
const makePageRenderer = ({computeContext}) => {
const makePageRenderer = ({ computeContext }) => {
// onPageLoad callback
const renderPage = async sink => {
const req = sink.request;
// according to the Apollo doc, client needs to be recreated on every request
// this avoids caching server side
const client = await createClient({req, computeContext});
const client = await createClient({ req, computeContext });
// Used by callbacks to handle side effects
// E.g storing the stylesheet generated by styled-components
@ -31,15 +31,13 @@ const makePageRenderer = ({computeContext}) => {
// middlewares at this point
// @see https://github.com/meteor/meteor-feature-requests/issues/174#issuecomment-441047495
const App = (
<AppGenerator req={req} apolloClient={client} context={context} />
);
const App = <AppGenerator req={req} apolloClient={client} context={context} />;
// run user registered callbacks that wraps the React app
const WrappedApp = runCallbacks({
name: 'router.server.wrapper',
iterator: App,
properties: {req, context, apolloClient: client},
properties: { req, context, apolloClient: client },
});
// equivalent to calling getDataFromTree and then renderToStringWithData
@ -67,7 +65,7 @@ const makePageRenderer = ({computeContext}) => {
runCallbacks({
name: 'router.server.postRender',
iterator: sink,
properties: {context},
properties: { context },
});
};
return renderPage;

View file

@ -1,14 +1,14 @@
// see https://github.com/apollographql/graphql-tools/blob/master/docs/source/schema-directives.md#marking-strings-for-internationalization
import {addGraphQLDirective, addGraphQLSchema} from '../modules/graphql';
import {SchemaDirectiveVisitor} from 'graphql-tools';
import {defaultFieldResolver} from 'graphql';
import {Collections} from '../modules/collections';
import {getSetting} from '../modules/settings';
import {debug} from '../modules/debug';
import { addGraphQLDirective, addGraphQLSchema } from '../modules/graphql';
import { SchemaDirectiveVisitor } from 'graphql-tools';
import { defaultFieldResolver } from 'graphql';
import { Collections } from '../modules/collections';
import { getSetting } from '../modules/settings';
import { debug } from '../modules/debug';
import Vulcan from '../modules/config';
import {isIntlField} from '../modules/intl';
import {Connectors} from './connectors';
import { isIntlField } from '../modules/intl';
import { Connectors } from './connectors';
import pickBy from 'lodash/pickBy';
/*
@ -32,16 +32,11 @@ Take an array of translations, a locale, and a default locale, and return a matc
*/
const getLocaleString = (translations, locale, defaultLocale) => {
const localeObject = translations.find(
translation => translation.locale === locale
);
const localeObject = translations.find(translation => translation.locale === locale);
const defaultLocaleObject = translations.find(
translation => translation.locale === defaultLocale
);
return (
(localeObject && localeObject.value) ||
(defaultLocaleObject && defaultLocaleObject.value)
);
return (localeObject && localeObject.value) || (defaultLocaleObject && defaultLocaleObject.value);
};
/*
@ -59,15 +54,12 @@ class IntlDirective extends SchemaDirectiveVisitor {
const defaultLocale = getSetting('locale');
const intlField = doc[`${name}_intl`];
// Return string in requested or default language, or else field's original value
return (
(intlField && getLocaleString(intlField, locale, defaultLocale)) ||
fieldValue
);
return (intlField && getLocaleString(intlField, locale, defaultLocale)) || fieldValue;
};
}
}
addGraphQLDirective({intl: IntlDirective});
addGraphQLDirective({ intl: IntlDirective });
addGraphQLSchema('directive @intl on FIELD_DEFINITION');
@ -79,7 +71,7 @@ Migration function
const migrateIntlFields = async defaultLocale => {
if (!defaultLocale) {
throw new Error(
"Please pass the id of the locale to which to migrate your current content (e.g. migrateIntlFields('en'))"
'Please pass the id of the locale to which to migrate your current content (e.g. migrateIntlFields(\'en\'))'
);
}
@ -101,31 +93,22 @@ const migrateIntlFields = async defaultLocale => {
const selector = {
$or: intlFieldsNames.map(f => {
return {
$and: [
{[`${f}`]: {$exists: true}},
{[`${f}_intl`]: {$exists: false}},
],
$and: [{ [`${f}`]: { $exists: true } }, { [`${f}_intl`]: { $exists: false } }],
};
}),
};
const documentsToMigrate = await Connectors.find(collection, selector);
if (documentsToMigrate.length) {
console.log(
`-> found ${documentsToMigrate.length} documents to migrate \n`
); // eslint-disable-line no-console
console.log(`-> found ${documentsToMigrate.length} documents to migrate \n`); // eslint-disable-line no-console
for (const doc of documentsToMigrate) {
console.log(`// Migrating document ${doc._id}`); // eslint-disable-line no-console
const modifier = {$push: {}};
const modifier = { $push: {} };
intlFieldsNames.forEach(f => {
if (doc[f] && !doc[`${f}_intl`]) {
const translationObject = {locale: defaultLocale, value: doc[f]};
console.log(
`-> Adding field ${f}_intl: ${JSON.stringify(
translationObject
)} `
); // eslint-disable-line no-console
const translationObject = { locale: defaultLocale, value: doc[f] };
console.log(`-> Adding field ${f}_intl: ${JSON.stringify(translationObject)} `); // eslint-disable-line no-console
modifier.$push[`${f}_intl`] = translationObject;
}
});
@ -133,11 +116,7 @@ const migrateIntlFields = async defaultLocale => {
if (!_.isEmpty(modifier.$push)) {
// update document
// eslint-disable-next-line no-await-in-loop
const n = await Connectors.update(
collection,
{_id: doc._id},
modifier
);
const n = await Connectors.update(collection, { _id: doc._id }, modifier);
console.log(`-> migrated ${n} documents \n`); // eslint-disable-line no-console
}
console.log('\n'); // eslint-disable-line no-console
@ -173,9 +152,7 @@ export const getHeaderLocale = (headers, userLocale) => {
// get locale from accepted-language header
if (headers['accept-language']) {
const acceptedLanguages = headers['accept-language']
.split(',')
.map(l => l.split(';')[0]);
const acceptedLanguages = headers['accept-language'].split(',').map(l => l.split(';')[0]);
acceptedLocale = acceptedLanguages[0]; // for now only use the highest-priority accepted language
}

View file

@ -3,8 +3,8 @@
Run a GraphQL request from the server with the proper context
*/
import {graphql} from 'graphql';
import {Collections} from '../modules/collections.js';
import { graphql } from 'graphql';
import { Collections } from '../modules/collections.js';
import DataLoader from 'dataloader';
import findByIds from '../modules/findbyids.js';
import {
@ -12,16 +12,16 @@ import {
extractFragmentName,
getFragmentText,
} from '../modules/fragments.js';
import {getSetting} from '../modules/settings';
import { getSetting } from '../modules/settings';
import merge from 'lodash/merge';
import {singleClientTemplate} from '../modules/graphql_templates';
import {Utils} from './utils';
import {GraphQLSchema} from '../modules/graphql';
import { singleClientTemplate } from '../modules/graphql_templates';
import { Utils } from './utils';
import { GraphQLSchema } from '../modules/graphql';
// note: if no context is passed, default to running requests with full admin privileges
export const runGraphQL = async (query, variables = {}, context) => {
const defaultContext = {
currentUser: {isAdmin: true},
currentUser: { isAdmin: true },
locale: getSetting('locale'),
};
const queryContext = merge(defaultContext, context);
@ -30,21 +30,14 @@ export const runGraphQL = async (query, variables = {}, context) => {
// within the scope of this specific request,
// decorate each collection with a new Dataloader object and add it to context
Collections.forEach(collection => {
collection.loader = new DataLoader(
ids => findByIds(collection, ids, queryContext),
{cache: true}
);
collection.loader = new DataLoader(ids => findByIds(collection, ids, queryContext), {
cache: true,
});
queryContext[collection.options.collectionName] = collection;
});
// see http://graphql.org/graphql-js/graphql/#graphql
const result = await graphql(
executableSchema,
query,
{},
queryContext,
variables
);
const result = await graphql(executableSchema, query, {}, queryContext, variables);
if (result.errors) {
// eslint-disable-next-line no-console
@ -65,7 +58,7 @@ Given a collection and a fragment, build a query to fetch one document.
If no fragment is passed, default to default fragment
*/
export const buildQuery = (collection, {fragmentName, fragmentText}) => {
export const buildQuery = (collection, { fragmentName, fragmentText }) => {
const collectionName = collection.options.collectionName;
const typeName = collection.options.typeName;
@ -100,16 +93,9 @@ Meteor.startup(() => {
Collections.forEach(collection => {
const typeName = collection.options.typeName;
collection.queryOne = async (
documentId,
{fragmentName, fragmentText, context}
) => {
const query = buildQuery(collection, {fragmentName, fragmentText});
const result = await runQuery(
query,
{input: {selector: {documentId}}},
context
);
collection.queryOne = async (documentId, { fragmentName, fragmentText, context }) => {
const query = buildQuery(collection, { fragmentName, fragmentText });
const result = await runQuery(query, { input: { selector: { documentId } } }, context);
return result.data[Utils.camelCaseify(typeName)].result;
};
});

View file

@ -9,7 +9,7 @@ Package.onUse(function(api) {
api.versionsFrom('1.6.1');
// note: if used, accounts-base should be loaded before vulcan:lib
api.use('accounts-base', {weak: true});
api.use('accounts-base', { weak: true });
var packages = [
'buffer@0.0.0', // see https://github.com/meteor/meteor/issues/8645

View file

@ -6,9 +6,9 @@ import {
initContext,
computeContextFromReq,
} from '../../lib/server/apollo-server';
import {GraphQLSchema} from '../../lib/modules/graphql';
import { GraphQLSchema } from '../../lib/modules/graphql';
import expect from 'expect';
import {executableSchema} from './fixtures/minimalSchema';
import { executableSchema } from './fixtures/minimalSchema';
const test = it; // TODO: just before we switch to jest
// @see https://www.apollographql.com/docs/apollo-server/features/testing.html

View file

@ -1,7 +1,7 @@
// blatantly stolen from https://www.apollographql.com/docs/graphql-tools/generate-schema.html
import find from 'lodash/find';
import filter from 'lodash/filter';
import {makeExecutableSchema} from 'graphql-tools';
import { makeExecutableSchema } from 'graphql-tools';
const typeDefs = `
type Author {
@ -34,27 +34,27 @@ const typeDefs = `
// example data
const authors = [
{id: 1, firstName: 'Tom', lastName: 'Coleman'},
{id: 2, firstName: 'Sashko', lastName: 'Stubailo'},
{id: 3, firstName: 'Mikhail', lastName: 'Novikov'},
{ id: 1, firstName: 'Tom', lastName: 'Coleman' },
{ id: 2, firstName: 'Sashko', lastName: 'Stubailo' },
{ id: 3, firstName: 'Mikhail', lastName: 'Novikov' },
];
const posts = [
{id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2},
{id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3},
{id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1},
{id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7},
{ id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 },
{ id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 },
{ id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 },
{ id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 },
];
const resolvers = {
Query: {
posts: () => posts,
author: (_, {id}) => find(authors, {id}),
author: (_, { id }) => find(authors, { id }),
},
Mutation: {
upvotePost: (_, {postId}) => {
const post = find(posts, {id: postId});
upvotePost: (_, { postId }) => {
const post = find(posts, { id: postId });
if (!post) {
throw new Error(`Couldn't find post with id ${postId}`);
}
@ -64,11 +64,11 @@ const resolvers = {
},
Author: {
posts: author => filter(posts, {authorId: author.id}),
posts: author => filter(posts, { authorId: author.id }),
},
Post: {
author: post => find(authors, {id: post.authorId}),
author: post => find(authors, { id: post.authorId }),
},
};

View file

@ -1,6 +1,6 @@
import expect from 'expect';
import {GraphQLSchema} from '../../lib/modules/graphql';
import { GraphQLSchema } from '../../lib/modules/graphql';
import initGraphQL from '../../lib/server/apollo-server/initGraphQL';
describe('vulcan:lib/graphql', function() {

View file

@ -55,26 +55,16 @@ import express from 'express';
import Stripe from 'stripe';
import Charges from '../../modules/charges/collection.js';
import Users from 'meteor/vulcan:users';
import {Products} from '../../modules/products.js';
import {Promise} from 'meteor/promise';
import { Products } from '../../modules/products.js';
import { Promise } from 'meteor/promise';
registerSetting('stripe', null, 'Stripe settings');
registerSetting('stripe.publishableKey', null, 'Publishable key', true);
registerSetting(
'stripe.publishableKeyTest',
null,
'Publishable key (test)',
true
);
registerSetting('stripe.publishableKeyTest', null, 'Publishable key (test)', true);
registerSetting('stripe.secretKey', null, 'Secret key');
registerSetting('stripe.secretKeyTest', null, 'Secret key (test)');
registerSetting('stripe.endpointSecret', null, 'Endpoint secret for webhook');
registerSetting(
'stripe.alwaysUseTest',
false,
'Always use test keys in all environments',
true
);
registerSetting('stripe.alwaysUseTest', false, 'Always use test keys in all environments', true);
const stripeSettings = getSetting('stripe');
@ -102,13 +92,7 @@ export const receiveAction = async args => {
document,
returnDocument = {};
const {
userId,
productKey,
associatedCollection,
associatedId,
properties,
} = args;
const { userId, productKey, associatedCollection, associatedId, properties } = args;
if (!stripeSettings) {
throw new Error('Please fill in your Stripe settings');
@ -117,7 +101,7 @@ export const receiveAction = async args => {
// if an associated collection name and document id have been provided,
// get the associated collection and document
if (associatedCollection && associatedId) {
collection = _.findWhere(Collections, {_name: associatedCollection});
collection = _.findWhere(Collections, { _name: associatedCollection });
document = await Connectors.get(collection, associatedId);
}
@ -193,7 +177,7 @@ Retrieve or create a Stripe customer
*/
export const getCustomer = async (user, token) => {
const {id} = token;
const { id } = token;
let customer;
@ -203,7 +187,7 @@ export const getCustomer = async (user, token) => {
} catch (error) {
// if user doesn't have a stripeCustomerId; or if id doesn't match up with Stripe
// create new customer object
const customerOptions = {email: user.email};
const customerOptions = { email: user.email };
if (id) {
customerOptions.source = id;
}
@ -213,7 +197,7 @@ export const getCustomer = async (user, token) => {
await updateMutator({
collection: Users,
documentId: user._id,
data: {stripeCustomerId: customer.id},
data: { stripeCustomerId: customer.id },
validate: false,
});
}
@ -226,18 +210,8 @@ export const getCustomer = async (user, token) => {
Create one-time charge.
*/
export const createCharge = async ({
user,
product,
collection,
document,
metadata,
args,
}) => {
const {
token,
/* userId, productKey, associatedId, properties, */ coupon,
} = args;
export const createCharge = async ({ user, product, collection, document, metadata, args }) => {
const { token, /* userId, productKey, associatedId, properties, */ coupon } = args;
const customer = await getCustomer(user, token);
@ -314,7 +288,7 @@ export const createSubscription = async ({
// eslint-disable-next-line no-unused-vars
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{plan: product.plan}],
items: [{ plan: product.plan }],
metadata,
...product.subscriptionProperties,
});
@ -410,9 +384,7 @@ const createOrRetrievePlan = async planObject => {
if (error.statusCode === 404) {
// eslint-disable-next-line no-console
console.log(
`Creating subscription plan ${
planObject.plan
} for ${(planObject.amount &&
`Creating subscription plan ${planObject.plan} for ${(planObject.amount &&
(planObject.amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: planObject.currency,
@ -435,17 +407,9 @@ export const createOrRetrieveSubscriptionPlan = async maybePlanObject =>
Process charges, subscriptions, etc. on Vulcan's side
*/
export const processAction = async ({
collection,
document,
stripeObject,
args,
user,
}) => {
export const processAction = async ({ collection, document, stripeObject, args, user }) => {
debug('');
debugGroup(
'--------------- start\x1b[35m processAction \x1b[0m ---------------'
);
debugGroup('--------------- start\x1b[35m processAction \x1b[0m ---------------');
debug(`Collection: ${collection.options.collectionName}`);
debug(`documentId: ${document._id}`);
debug(`Charge: ${stripeObject}`);
@ -462,9 +426,7 @@ export const processAction = async ({
if (existingCharge) {
// eslint-disable-next-line no-console
console.log(
`// Charge with Stripe id ${
stripeObject.id
} already exists in db; aborting processAction`
`// Charge with Stripe id ${stripeObject.id} already exists in db; aborting processAction`
);
return collection && document ? document : {};
}
@ -514,13 +476,13 @@ export const processAction = async ({
? [...document.chargeIds, chargeSaved._id]
: [chargeSaved._id];
let data = {chargeIds};
let data = { chargeIds };
// run collection.charge.sync callbacks
data = await runCallbacks({
name: 'stripe.process.sync',
iterator: data,
properties: {collection, document, chargeDoc, user},
properties: { collection, document, chargeDoc, user },
});
const updateResult = await updateMutator({
@ -583,11 +545,7 @@ app.post('/stripe', addRawBody, async function(req, res) {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
stripeSettings.endpointSecret
);
const event = stripe.webhooks.constructEvent(req.rawBody, sig, stripeSettings.endpointSecret);
// eslint-disable-next-line no-console
console.log('event ///////////////////');
@ -615,20 +573,13 @@ app.post('/stripe', addRawBody, async function(req, res) {
console.log(invoice);
// look up corresponding subscription
const subscription = await stripe.subscriptions.retrieve(
invoice.subscription
);
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
// eslint-disable-next-line no-console
console.log('////// subscription');
// eslint-disable-next-line no-console
console.log(subscription);
const {
userId,
productKey,
associatedCollection,
associatedId,
} = subscription.metadata;
const { userId, productKey, associatedCollection, associatedId } = subscription.metadata;
if (associatedCollection && associatedId) {
const collection = _.findWhere(Collections, {
@ -653,7 +604,7 @@ app.post('/stripe', addRawBody, async function(req, res) {
livemode: subscription.livemode,
};
processAction({collection, document, stripeObject: charge, args});
processAction({ collection, document, stripeObject: charge, args });
}
} catch (error) {
// eslint-disable-next-line no-console
@ -762,14 +713,14 @@ webAppConnectHandlersUse(Meteor.bindEnvironment(app), {
Meteor.startup(() => {
registerCallback({
name: 'stripe.receive.sync',
description: "Modify any metadata before calling Stripe's API",
description: 'Modify any metadata before calling Stripe\'s API',
arguments: [
{metadata: 'Metadata about the action'},
{user: 'The user'},
{product: 'Product created with addProduct'},
{collection: 'Associated collection of the charge'},
{document: 'Associated document in collection to the charge'},
{args: 'Original mutation arguments'},
{ metadata: 'Metadata about the action' },
{ user: 'The user' },
{ product: 'Product created with addProduct' },
{ collection: 'Associated collection of the charge' },
{ document: 'Associated document in collection to the charge' },
{ args: 'Original mutation arguments' },
],
runs: 'sync',
newSyntax: true,
@ -778,14 +729,14 @@ Meteor.startup(() => {
registerCallback({
name: 'stripe.receive.async',
description: "Run after calling Stripe's API",
description: 'Run after calling Stripe\'s API',
arguments: [
{metadata: 'Metadata about the charge'},
{user: 'The user'},
{product: 'Product created with addProduct'},
{collection: 'Associated collection of the charge'},
{document: 'Associated document in collection to the charge'},
{args: 'Original mutation arguments'},
{ metadata: 'Metadata about the charge' },
{ user: 'The user' },
{ product: 'Product created with addProduct' },
{ collection: 'Associated collection of the charge' },
{ document: 'Associated document in collection to the charge' },
{ args: 'Original mutation arguments' },
],
runs: 'sync',
newSyntax: true,
@ -793,14 +744,13 @@ Meteor.startup(() => {
registerCallback({
name: 'stripe.charge.async',
description:
'Perform operations immediately after the stripe subscription has completed',
description: 'Perform operations immediately after the stripe subscription has completed',
arguments: [
{charge: 'The charge'},
{collection: 'Associated collection of the subscription'},
{document: 'Associated document in collection to the charge'},
{args: 'Original mutation arguments'},
{user: 'The user'},
{ charge: 'The charge' },
{ collection: 'Associated collection of the subscription' },
{ document: 'Associated document in collection to the charge' },
{ args: 'Original mutation arguments' },
{ user: 'The user' },
],
runs: 'async',
newSyntax: true,
@ -808,14 +758,13 @@ Meteor.startup(() => {
registerCallback({
name: 'stripe.subscribe.async',
description:
'Perform operations immediately after the stripe subscription has completed',
description: 'Perform operations immediately after the stripe subscription has completed',
arguments: [
{subscription: 'The subscription'},
{collection: 'Associated collection of the subscription'},
{document: 'Associated document in collection to the charge'},
{args: 'Original mutation arguments'},
{user: 'The user'},
{ subscription: 'The subscription' },
{ collection: 'Associated collection of the subscription' },
{ document: 'Associated document in collection to the charge' },
{ args: 'Original mutation arguments' },
{ user: 'The user' },
],
runs: 'async',
newSyntax: true,
@ -826,13 +775,12 @@ Meteor.startup(() => {
description: 'Modify any metadata before sending the charge to stripe',
arguments: [
{
modifier:
'The modifier object used to update the associated collection',
modifier: 'The modifier object used to update the associated collection',
},
{collection: 'Collection associated to the product'},
{document: 'Associated document'},
{chargeDoc: "Charge document returned by Stripe's API"},
{user: 'The user'},
{ collection: 'Collection associated to the product' },
{ document: 'Associated document' },
{ chargeDoc: 'Charge document returned by Stripe\'s API' },
{ user: 'The user' },
],
runs: 'sync',
returns: 'The modified arguments to be sent to stripe',
@ -842,10 +790,10 @@ Meteor.startup(() => {
name: 'stripe.process.async',
description: 'Modify any metadata before sending the charge to stripe',
arguments: [
{collection: 'Collection associated to the product'},
{document: 'Associated document'},
{chargeDoc: "Charge document returned by Stripe's API"},
{user: 'The user'},
{ collection: 'Collection associated to the product' },
{ document: 'Associated document' },
{ chargeDoc: 'Charge document returned by Stripe\'s API' },
{ user: 'The user' },
],
runs: 'async',
returns: 'The modified arguments to be sent to stripe',
@ -858,11 +806,7 @@ Meteor.startup(() => {
Promise.awaitAll(
Object.keys(Products)
// Filter out function type products and those without a plan defined (non-subscription)
.filter(
productKey =>
typeof Products[productKey] === 'object' &&
Products[productKey].plan
)
.filter(productKey => typeof Products[productKey] === 'object' && Products[productKey].plan)
.map(productKey => createOrRetrievePlan(Products[productKey]))
);
// eslint-disable-next-line no-console

View file

@ -1,7 +1,7 @@
import React from 'react';
import {Provider} from 'react-redux';
import {addCallback} from 'meteor/vulcan:core';
import {initStore} from '../modules/redux';
import { Provider } from 'react-redux';
import { addCallback } from 'meteor/vulcan:core';
import { initStore } from '../modules/redux';
const setupRedux = () => {
const store = initStore();

View file

@ -1,4 +1,4 @@
import {createStore, applyMiddleware, compose, combineReducers} from 'redux';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import _isEmpty from 'lodash/isEmpty';
// TODO: now we should add some callback call to add the store to
// Apollo SSR + client side too
@ -48,9 +48,7 @@ export const configureStore = (
}
if (options.reducers) {
reducers =
typeof options.reducers === 'object'
? combineReducers(options.reducers)
: options.reducers;
typeof options.reducers === 'object' ? combineReducers(options.reducers) : options.reducers;
}
this.replaceReducer(reducers);
return store;
@ -63,7 +61,7 @@ export const configureStore = (
// **Notes: server side, addAction to server share with every req**
let actions = {};
export const addAction = addedAction => {
actions = {...actions, ...addedAction};
actions = { ...actions, ...addedAction };
return actions;
};
export const getActions = () => actions;
@ -73,7 +71,7 @@ export const getActions = () => actions;
let reducers = {};
export const addReducer = addedReducer => {
reducers = {...reducers, ...addedReducer};
reducers = { ...reducers, ...addedReducer };
return reducers;
};
export const getReducers = () => reducers;

View file

@ -1,7 +1,7 @@
import React from 'react';
import {Provider} from 'react-redux';
import {addCallback} from 'meteor/vulcan:core';
import {initStore} from '../modules/redux';
import { Provider } from 'react-redux';
import { addCallback } from 'meteor/vulcan:core';
import { initStore } from '../modules/redux';
const setupRedux = () => {
const store = initStore();

View file

@ -1,9 +1,9 @@
// Setup SSR
import {ServerStyleSheet} from 'styled-components';
import {addCallback} from 'meteor/vulcan:core';
import { ServerStyleSheet } from 'styled-components';
import { addCallback } from 'meteor/vulcan:core';
const setupStyledComponents = () => {
addCallback('router.server.wrapper', function collectStyles(app, {context}) {
addCallback('router.server.wrapper', function collectStyles(app, { context }) {
const stylesheet = new ServerStyleSheet();
// @see https://www.styled-components.com/docs/advanced/#example
const wrappedApp = stylesheet.collectStyles(app);
@ -12,10 +12,7 @@ const setupStyledComponents = () => {
return wrappedApp;
});
addCallback('router.server.postRender', function appendStyleTags(
sink,
{context}
) {
addCallback('router.server.postRender', function appendStyleTags(sink, { context }) {
sink.appendToHead(context.stylesheet.getStyleTags());
return sink;
});

View file

@ -9,9 +9,9 @@ import Adapter from 'enzyme-adapter-react-16.3';
const initComponentTest = () => {
// setup enzyme
Enzyme.configure({adapter: new Adapter()});
Enzyme.configure({ adapter: new Adapter() });
// and then load them in the app so that <Component.Whatever /> is defined
import {populateComponentsApp, initializeFragments} from 'meteor/vulcan:lib';
import { populateComponentsApp, initializeFragments } from 'meteor/vulcan:lib';
// we need registered fragments to be initialized because populateComponentsApp will run
// hocs, like withUpdate, that rely on fragments
initializeFragments();

View file

@ -1,4 +1,4 @@
import {Utils} from 'meteor/vulcan:lib';
import { Utils } from 'meteor/vulcan:lib';
import Users from './collection.js';
import moment from 'moment';
import _ from 'underscore';
@ -32,12 +32,7 @@ Users.getUser = function(userOrUserId) {
Users.getUserName = function(user) {
try {
if (user.username) return user.username;
if (
user &&
user.services &&
user.services.twitter &&
user.services.twitter.screenName
)
if (user && user.services && user.services.twitter && user.services.twitter.screenName)
return user.services.twitter.screenName;
} catch (error) {
console.log(error); // eslint-disable-line
@ -189,7 +184,7 @@ Users.hasCompletedProfile = function(user) {
///////////////////
Users.findLast = function(user, collection) {
return collection.findOne({userId: user._id}, {sort: {createdAt: -1}});
return collection.findOne({ userId: user._id }, { sort: { createdAt: -1 } });
};
Users.timeSinceLast = function(user, collection) {
@ -263,5 +258,5 @@ Users.getRequiredFields = function() {
// };
Users.findByEmail = function(email) {
return Users.findOne({email: email});
return Users.findOne({ email: email });
};

View file

@ -1,4 +1,4 @@
import {addGraphQLResolvers, Connectors} from 'meteor/vulcan:lib';
import { addGraphQLResolvers, Connectors } from 'meteor/vulcan:lib';
const specificResolvers = {
Query: {
@ -26,30 +26,26 @@ const defaultOptions = {
const resolvers = {
multi: {
async resolver(root, {input = {}}, {currentUser, Users}, {cacheControl}) {
const {terms = {}, enableCache = false, enableTotal = true} = input;
async resolver(root, { input = {} }, { currentUser, Users }, { cacheControl }) {
const { terms = {}, enableCache = false, enableTotal = true } = input;
if (cacheControl && enableCache) {
const maxAge = defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({maxAge});
cacheControl.setCacheHint({ maxAge });
}
// get selector and options from terms and perform Mongo query
let {selector, options} = await Users.getParameters(terms);
let { selector, options } = await Users.getParameters(terms);
options.skip = terms.offset;
const users = await Connectors.find(Users, selector, options);
// restrict documents fields
const restrictedUsers = Users.restrictViewableFields(
currentUser,
Users,
users
);
const restrictedUsers = Users.restrictViewableFields(currentUser, Users, users);
// prime the cache
restrictedUsers.forEach(user => Users.loader.prime(user._id, user));
const data = {results: restrictedUsers};
const data = { results: restrictedUsers };
if (enableTotal) {
// get total count of documents matching the selector
@ -61,22 +57,22 @@ const resolvers = {
},
single: {
async resolver(root, {input = {}}, {currentUser, Users}, {cacheControl}) {
const {selector = {}, enableCache = false} = input;
const {documentId, slug} = selector;
async resolver(root, { input = {} }, { currentUser, Users }, { cacheControl }) {
const { selector = {}, enableCache = false } = input;
const { documentId, slug } = selector;
if (cacheControl && enableCache) {
const maxAge = defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({maxAge});
cacheControl.setCacheHint({ maxAge });
}
// don't use Dataloader if user is selected by slug
const user = documentId
? await Users.loader.load(documentId)
: slug
? await Connectors.get(Users, {slug})
? await Connectors.get(Users, { slug })
: await Connectors.get(Users);
return {result: Users.restrictViewableFields(currentUser, Users, user)};
return { result: Users.restrictViewableFields(currentUser, Users, user) };
},
},
};