diff --git a/packages/vulcan-core/lib/modules/components/Datatable.jsx b/packages/vulcan-core/lib/modules/components/Datatable.jsx index 44f8e7e2b..864cb9f63 100644 --- a/packages/vulcan-core/lib/modules/components/Datatable.jsx +++ b/packages/vulcan-core/lib/modules/components/Datatable.jsx @@ -2,7 +2,7 @@ import { registerComponent, Components, getCollection, Utils } from 'meteor/vulc import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import withCurrentUser from '../containers/withCurrentUser.js'; -import withList from '../containers/withList.js'; +import withList from '../containers/withMulti.js'; import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n'; import { getFieldValue } from './Card.jsx'; diff --git a/packages/vulcan-core/lib/modules/containers/withList.js b/packages/vulcan-core/lib/modules/containers/withMulti.js similarity index 75% rename from packages/vulcan-core/lib/modules/containers/withList.js rename to packages/vulcan-core/lib/modules/containers/withMulti.js index 847456a9e..9681f3943 100644 --- a/packages/vulcan-core/lib/modules/containers/withList.js +++ b/packages/vulcan-core/lib/modules/containers/withMulti.js @@ -33,34 +33,37 @@ Terms object can have the following properties: - limit: String */ - + import React, { Component } from 'react'; import { withApollo, graphql } from 'react-apollo'; import gql from 'graphql-tag'; import update from 'immutability-helper'; -import { getSetting, getFragment, getFragmentName, getCollection } from 'meteor/vulcan:core'; +import { + getSetting, + getFragment, + getFragmentName, + getCollection, + Utils, + multiClientTemplate, +} from 'meteor/vulcan:lib'; import Mingo from 'mingo'; import compose from 'recompose/compose'; import withState from 'recompose/withState'; -const withList = (options) => { - +export default function withMulti (options) { // console.log(options) - + const { collectionName, limit = 10, pollInterval = getSetting('pollInterval', 20000), - totalResolver = true, enableCache = false, extraQueries, } = options; - + const collection = options.collection || getCollection(collectionName); - - const queryName = options.queryName || `${collection.options.collectionName}ListQuery`; - const listResolverName = collection.options.resolvers.list && collection.options.resolvers.list.name; - const totalResolverName = collection.options.resolvers.total && collection.options.resolvers.total.name; + const typeName = collection.options.typeName; + const resolverName = `${Utils.camelCaseify(typeName)}s`; let fragment; @@ -75,48 +78,35 @@ const withList = (options) => { const fragmentName = getFragmentName(fragment); // build graphql query from options - const query = gql` - query ${queryName}($terms: JSON, $enableCache: Boolean) { - ${totalResolver ? `${totalResolverName}(terms: $terms, enableCache: $enableCache)` : ``} - ${listResolverName}(terms: $terms, enableCache: $enableCache) { - __typename - ...${fragmentName} - } - ${extraQueries || ''} - } - ${fragment} - `; + const query = gql`${multiClientTemplate({ typeName, fragment, extraQueries })}${fragment}`; return compose( - // wrap component with Apollo HoC to give it access to the store - withApollo, + withApollo, // wrap component with HoC that manages the terms object via its state withState('paginationTerms', 'setPaginationTerms', props => { - // get initial limit from props, or else options - const paginationLimit = props.terms && props.terms.limit || limit; + const paginationLimit = (props.terms && props.terms.limit) || limit; const paginationTerms = { - limit: paginationLimit, - itemsPerPage: paginationLimit, + limit: paginationLimit, + itemsPerPage: paginationLimit, }; - + return paginationTerms; }), // wrap component with graphql HoC graphql( - query, { - alias: 'withList', - + alias: `with${typeName}s`, + // 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: { @@ -125,40 +115,39 @@ const withList = (options) => { }, // note: pollInterval can be set to 0 to disable polling (20s by default) pollInterval, - reducer: (previousResults, action) => { + // reducer: (previousResults, action) => { - // see queryReducer function defined below - return queryReducer(previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName, apolloClient); - - }, + // // see queryReducer function defined below + // return queryReducer(previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName, apolloClient); + + // }, }; if (options.fetchPolicy) { - graphQLOptions.fetchPolicy = options.fetchPolicy + graphQLOptions.fetchPolicy = options.fetchPolicy; } // 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; }, // define props returned by graphql HoC props(props) { - // 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[listResolverName], - totalCount = props.data[totalResolverName], - networkStatus = props.data.networkStatus, - loadingInitial = props.data.networkStatus === 1, - loading = props.data.networkStatus === 1, - loadingMore = props.data.networkStatus === 2, - error = props.data.error, - propertyName = options.propertyName || 'results'; + // results = Utils.convertDates(collection, props.data[listResolverName]), + results = props.data[resolverName].results, + totalCount = props.data[resolverName].totalCount, + networkStatus = props.data.networkStatus, + loadingInitial = props.data.networkStatus === 1, + loading = props.data.networkStatus === 1, + loadingMore = props.data.networkStatus === 2, + error = props.data.error, + propertyName = options.propertyName || 'results'; if (error) { // eslint-disable-next-line no-console @@ -171,7 +160,7 @@ const withList = (options) => { loading, loadingInitial, loadingMore, - [ propertyName ]: results, + [propertyName]: results, totalCount, refetch, networkStatus, @@ -181,18 +170,26 @@ const withList = (options) => { // regular load more (reload everything) loadMore(providedTerms) { // if new terms are provided by presentational component use them, else default to incrementing current limit once - const newTerms = typeof providedTerms === 'undefined' ? { /*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms, limit: results.length + props.ownProps.paginationTerms.itemsPerPage } : providedTerms; - + const newTerms = + typeof providedTerms === 'undefined' + ? { + /*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms, + limit: results.length + props.ownProps.paginationTerms.itemsPerPage, + } + : providedTerms; + props.ownProps.setPaginationTerms(newTerms); }, // incremental loading version (only load new content) // note: not compatible with polling loadMoreInc(providedTerms) { - // get terms passed as argument or else just default to incrementing the offset - const newTerms = typeof providedTerms === 'undefined' ? { ...props.ownProps.terms, ...props.ownProps.paginationTerms, offset: results.length } : providedTerms; - + const newTerms = + typeof providedTerms === 'undefined' + ? { ...props.ownProps.terms, ...props.ownProps.paginationTerms, offset: results.length } + : providedTerms; + return props.data.fetchMore({ variables: { terms: newTerms }, // ??? not sure about 'terms: newTerms' updateQuery(previousResults, { fetchMoreResult }) { @@ -201,9 +198,12 @@ const withList = (options) => { return previousResults; } const newResults = {}; - newResults[listResolverName] = [...previousResults[listResolverName], ...fetchMoreResult.data[listResolverName]]; + newResults[resolverName] = [ + ...previousResults[resolverName], + ...fetchMoreResult.data[resolverName], + ]; // return the previous results "augmented" with more - return {...previousResults, ...newResults}; + return { ...previousResults, ...newResults }; }, }); }, @@ -217,12 +217,19 @@ const withList = (options) => { } ) ); -} - +}; // define query reducer separately -const queryReducer = (previousResults, action, collection, mergedTerms, listResolverName, totalResolverName, queryName, apolloClient) => { - +const queryReducer = ( + previousResults, + action, + collection, + mergedTerms, + listResolverName, + totalResolverName, + queryName, + apolloClient +) => { // if collection has no mutations defined, just return previous results if (!collection.options.mutations) { return previousResults; @@ -245,19 +252,18 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso const listWithoutDocument = results[listResolverName].filter(doc => doc._id !== document._id); const newResults = update(results, { [listResolverName]: { $set: listWithoutDocument }, // ex: postsList - [totalResolverName]: { $set: results[totalResolverName] - 1 } // ex: postsListTotal + [totalResolverName]: { $set: results[totalResolverName] - 1 }, // ex: postsListTotal }); return newResults; - } + }; // add document to a results object const addToResults = (results, document) => { - return update(results, { [listResolverName]: { $unshift: [document] }, - [totalResolverName]: { $set: results[totalResolverName] + 1 } + [totalResolverName]: { $set: results[totalResolverName] + 1 }, }); - } + }; // reorder results according to a sort const reorderResults = (results, sort) => { @@ -268,7 +274,7 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso const sortedList = cursor.sort(sort).all(); results[listResolverName] = sortedList; return results; - } + }; // console.log('// withList reducer'); // console.log('queryName: ', queryName); @@ -280,7 +286,6 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso // console.log('action: ', action); switch (action.operationName) { - case newMutationName: // if new document belongs to current list (based on view selector), add it const newDocument = action.result.data[newMutationName]; @@ -297,7 +302,7 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso const editedDocument = action.result.data[editMutationName]; if (mingoQuery.test(editedDocument)) { // edited document belongs to the list - if (!_.findWhere(previousResults[listResolverName], {_id: editedDocument._id})) { + if (!_.findWhere(previousResults[listResolverName], { _id: editedDocument._id })) { // if document wasn't already in list, add it newResults = addToResults(previousResults, editedDocument); } @@ -319,7 +324,7 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso // console.log('removedDocument: ', removedDocument) break; - default: + default: // console.log('** no action **') return previousResults; } @@ -332,8 +337,5 @@ const queryReducer = (previousResults, action, collection, mergedTerms, listReso return { [listResolverName]: [...newResults[listResolverName]], [totalResolverName]: newResults[totalResolverName], - } - -} - -export default withList; + }; +}; diff --git a/packages/vulcan-core/lib/modules/containers/withDocument.js b/packages/vulcan-core/lib/modules/containers/withSingle.js similarity index 61% rename from packages/vulcan-core/lib/modules/containers/withDocument.js rename to packages/vulcan-core/lib/modules/containers/withSingle.js index f5133b327..77f41cf4e 100644 --- a/packages/vulcan-core/lib/modules/containers/withDocument.js +++ b/packages/vulcan-core/lib/modules/containers/withSingle.js @@ -2,16 +2,21 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; -import { getSetting, getFragment, getFragmentName, getCollection } from 'meteor/vulcan:core'; +import { getSetting, getFragment, getFragmentName, getCollection, singleClientTemplate } from 'meteor/vulcan:lib'; -export default function withDocument (options) { - - const { collectionName, pollInterval = getSetting('pollInterval', 20000), enableCache = false, extraQueries } = options; +export default function withSingle(options) { + + const { + collectionName, + pollInterval = getSetting('pollInterval', 20000), + enableCache = false, + extraQueries, + } = options; const collection = options.collection || getCollection(collectionName); - const queryName = options.queryName || `${collection.options.collectionName}SingleQuery`; - const singleResolverName = collection.options.resolvers.single && collection.options.resolvers.single.name; - + const typeName = collection.options.typeName; + const resolverName = typeName; + let fragment; if (options.fragment) { @@ -24,24 +29,17 @@ export default function withDocument (options) { const fragmentName = getFragmentName(fragment); - return graphql(gql` - query ${queryName}($documentId: String, $slug: String, $enableCache: Boolean) { - ${singleResolverName}(documentId: $documentId, slug: $slug, enableCache: $enableCache) { - __typename - ...${fragmentName} - } - ${extraQueries || ''} - } - ${fragment} - `, { - alias: 'withDocument', - + const query = gql`${singleClientTemplate({ typeName, fragment, extraQueries })}${fragment}`; + + return graphql(query, { + alias: `with${typeName}`, + options({ documentId, slug }) { const graphQLOptions = { - variables: { - documentId: documentId, - slug: slug, - enableCache, + variables: { + documentId: documentId, + slug: slug, + enableCache, }, pollInterval, // note: pollInterval can be set to 0 to disable polling (20s by default) }; @@ -54,12 +52,12 @@ export default function withDocument (options) { }, props: returnedProps => { const { /* ownProps, */ data } = returnedProps; - + const propertyName = options.propertyName || 'document'; const props = { loading: data.loading, // document: Utils.convertDates(collection, data[singleResolverName]), - [ propertyName ]: data[singleResolverName], + [propertyName]: data[resolverName].result, fragmentName, fragment, data, diff --git a/packages/vulcan-core/lib/modules/default_resolvers.js b/packages/vulcan-core/lib/modules/default_resolvers.js index 18004ea6b..dd2e6d1a2 100644 --- a/packages/vulcan-core/lib/modules/default_resolvers.js +++ b/packages/vulcan-core/lib/modules/default_resolvers.js @@ -69,8 +69,11 @@ export const getDefaultResolvers = (collectionName, resolverOptions = defaultOpt debug(`--------------- end \x1b[35m${typeName} list\x1b[0m resolver ---------------`); debug(''); + // get total count of documents matching the selector + const totalCount = await Connectors.count(collection, selector); + // return results - return restrictedDocs; + return { results: restrictedDocs, totalCount }; }, }, @@ -118,31 +121,9 @@ export const getDefaultResolvers = (collectionName, resolverOptions = defaultOpt debug(''); // filter out disallowed properties and return resulting document - return restrictedDoc; + return { result: restrictedDoc }; }, }, - - // resolver for returning the total number of documents matching a set of query terms - - // total: { - // name: resolverOptions.legacy ? `${typeName}Total` : `total${typeName}s`, - - // description: `The total count of ${typeName} documents matching a set of query terms`, - - // async resolver(root, { terms, enableCache }, context, { cacheControl }) { - // if (cacheControl && enableCache) { - // const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge; - // cacheControl.setCacheHint({ maxAge }); - // } - - // const collection = context[collectionName]; - - // const { selector } = await collection.getParameters(terms, {}, context); - - // const total = await Connectors.count(collection, selector); - - // return total; - // }, - // }, + }; }; diff --git a/packages/vulcan-core/lib/modules/index.js b/packages/vulcan-core/lib/modules/index.js index 1985fff5d..e97b5aad4 100644 --- a/packages/vulcan-core/lib/modules/index.js +++ b/packages/vulcan-core/lib/modules/index.js @@ -26,8 +26,8 @@ export { default as RouterHook } from './components/RouterHook.jsx'; export { default as withAccess } from "./containers/withAccess.js"; export { default as withMessages } from "./containers/withMessages.js"; -export { default as withList } from './containers/withList.js'; -export { default as withDocument } from './containers/withDocument.js'; +export { default as withMulti } from './containers/withMulti.js'; +export { default as withSingle } from './containers/withSingle.js'; export { default as withCreate } from './containers/withCreate.js'; export { default as withUpdate } from './containers/withUpdate.js'; export { default as withDelete } from './containers/withDelete.js'; @@ -40,3 +40,5 @@ export { default as withUpsert } from './containers/withUpsert.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'; diff --git a/packages/vulcan-lib/lib/modules/graphql_templates.js b/packages/vulcan-lib/lib/modules/graphql_templates.js index 0a23c64f0..4287412dd 100644 --- a/packages/vulcan-lib/lib/modules/graphql_templates.js +++ b/packages/vulcan-lib/lib/modules/graphql_templates.js @@ -1,4 +1,5 @@ import { Utils } from './utils'; +import { getFragmentName } from './fragments'; export const convertToGraphQL = (fields, indentation) => { return fields.length > 0 ? fields.map(f => fieldTemplate(f, indentation)).join(`\n`) : ''; @@ -193,13 +194,13 @@ export const multiInputTemplate = ({ typeName }) => The type for the return value when querying for a single document type SingleMovieOuput{ - data: Movie + result: Movie } */ export const singleOutputTemplate = ({ typeName }) => `type Single${typeName}Output{ - data: ${typeName} + result: ${typeName} }`; /* @@ -207,17 +208,42 @@ export const singleOutputTemplate = ({ typeName }) => The type for the return value when querying for multiple documents type MultiMovieOuput{ - data: [Movie] + results: [Movie] totalCount: Int } */ export const multiOutputTemplate = ({ typeName }) => `type Multi${typeName}Output{ - data: [${typeName}] + results: [${typeName}] totalCount: Int }`; +/* ------------------------------------- Query Queries ------------------------------------- */ + +export const singleClientTemplate = ({ typeName, fragment, extraQueries }) => +`query Single${typeName}Query($input: Single${typeName}Input) { + ${Utils.camelCaseify(typeName)}(input: $input) { + __typename + result { + ...${getFragmentName(fragment)} + } + } + ${extraQueries ? extraQueries : ''} +}`; + +export const multiClientTemplate = ({ typeName, fragment, extraQueries }) => +`query Multi${typeName}Query($input: Multi${typeName}Input) { + ${Utils.camelCaseify(typeName)}s(input: $input) { + __typename + results { + ...${getFragmentName(fragment)} + } + totalCount + } + ${extraQueries ? extraQueries : ''} +}`; + /* ------------------------------------- Mutation Types ------------------------------------- */ /* @@ -369,3 +395,5 @@ export const mutationOutputTemplate = ({ typeName }) => `type ${typeName}Output{ data: ${typeName} }`; + +/* ------------------------------------- Mutation Queries ------------------------------------- */ diff --git a/packages/vulcan-lib/lib/modules/index.js b/packages/vulcan-lib/lib/modules/index.js index 6cb45833a..4f69aeb0f 100644 --- a/packages/vulcan-lib/lib/modules/index.js +++ b/packages/vulcan-lib/lib/modules/index.js @@ -29,4 +29,5 @@ export * from './startup.js'; export * from './errors.js'; export * from './intl.js'; export * from './detect_locale.js'; +export * from './graphql_templates.js'; // export * from './resolvers.js';