diff --git a/packages/vulcan-core/lib/modules/containers/withMulti.js b/packages/vulcan-core/lib/modules/containers/withMulti.js index 0500c218f..22f36c5e0 100644 --- a/packages/vulcan-core/lib/modules/containers/withMulti.js +++ b/packages/vulcan-core/lib/modules/containers/withMulti.js @@ -61,7 +61,7 @@ export default function withMulti(options) { const { fragmentName, fragment } = extractFragmentInfo(options, collectionName); const typeName = collection.options.typeName; - const resolverName = Utils.camelCaseify(Utils.pluralize(typeName)); + const resolverName = collection.options.multiResolverName; // build graphql query from options const query = gql` @@ -189,7 +189,7 @@ export default function withMulti(options) { : providedTerms; return props.data.fetchMore({ - variables: { terms: newTerms }, // ??? not sure about 'terms: newTerms' + variables: { input: { terms: newTerms } }, // ??? not sure about 'terms: newTerms' updateQuery(previousResults, { fetchMoreResult }) { // no more post to fetch if (!fetchMoreResult.data) { diff --git a/packages/vulcan-core/lib/modules/default_resolvers.js b/packages/vulcan-core/lib/modules/default_resolvers.js index 7c675ae1b..449532fda 100644 --- a/packages/vulcan-core/lib/modules/default_resolvers.js +++ b/packages/vulcan-core/lib/modules/default_resolvers.js @@ -94,7 +94,7 @@ export function getDefaultResolvers(options) { description: `A single ${typeName} document fetched by ID or slug`, async resolver(root, { input = {} }, context, { cacheControl }) { - const { selector = {}, enableCache = false } = input; + const { selector = {}, enableCache = false, allowNull = false } = input; debug(''); debugGroup(`--------------- start \x1b[35m${typeName} Single Resolver\x1b[0m ---------------`); @@ -116,8 +116,12 @@ export function getDefaultResolvers(options) { : await Connectors.get(collection, selector); if (!doc) { - const MissingDocumentError = createError('app.missing_document', { message: 'app.missing_document' }); - throw new MissingDocumentError({ data: { documentId, slug } }); + if (allowNull) { + return { result: null }; + } else { + const MissingDocumentError = createError('app.missing_document', { message: 'app.missing_document' }); + throw new MissingDocumentError({ data: { documentId, slug } }); + } } // if collection has a checkAccess function defined, use it to perform a check on the current document @@ -137,4 +141,4 @@ export function getDefaultResolvers(options) { }, }, }; -} \ No newline at end of file +} diff --git a/packages/vulcan-lib/lib/modules/collections.js b/packages/vulcan-lib/lib/modules/collections.js index 34986a7cf..1fa1f809c 100644 --- a/packages/vulcan-lib/lib/modules/collections.js +++ b/packages/vulcan-lib/lib/modules/collections.js @@ -37,7 +37,7 @@ Mongo.Collection.prototype.attachSchema = function (schemaOrFields) { } else { this.simpleSchema().extend(schemaOrFields) } -} +}; /** * @summary Add an additional field (or an array of fields) to a schema. @@ -134,6 +134,8 @@ export const createCollection = options => { // add typeName if missing collection.typeName = typeName; collection.options.typeName = typeName; + collection.options.singleResolverName = Utils.camelCaseify(typeName); + collection.options.multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName)); // add collectionName if missing collection.collectionName = collectionName; @@ -158,13 +160,13 @@ export const createCollection = options => { hidden: true, type: Array, isIntlData: true, - } + }; delete schema[`${fieldName}_intl`].intl; schema[`${fieldName}_intl.$`] = { type: getIntlString(), - } + }; // if original field is required, enable custom validation function instead of `optional` property if (!schema[fieldName].optional) { @@ -287,9 +289,9 @@ export const createCollection = options => { // console.log(parameters); return parameters; - } + }; Collections.push(collection); return collection; -} +}; diff --git a/packages/vulcan-lib/lib/modules/config.js b/packages/vulcan-lib/lib/modules/config.js index 0329aee38..27d60550e 100644 --- a/packages/vulcan-lib/lib/modules/config.js +++ b/packages/vulcan-lib/lib/modules/config.js @@ -50,6 +50,7 @@ SimpleSchema.extendOptions([ 'options', // form options 'query', // field-specific data loading query 'selectable', // field can be used as part of a selector when querying for data + 'unique', // field can be used as part of a selectorUnique when querying for data 'orderable', // field can be used to order results when querying for data 'intl', // set to `true` to make a field international diff --git a/packages/vulcan-lib/lib/modules/graphql.js b/packages/vulcan-lib/lib/modules/graphql.js index 3e0ac4f15..5704ec0ce 100644 --- a/packages/vulcan-lib/lib/modules/graphql.js +++ b/packages/vulcan-lib/lib/modules/graphql.js @@ -125,7 +125,7 @@ export const GraphQLSchema = { this.directives = deepmerge(this.directives, directive); }, - // for a given schema, return main type fields, selector fields, + // for a given schema, return main type fields, selector fields, // unique selector fields, orderBy fields, creatable fields, and updatable fields getFields(schema, typeName) { const fields = { @@ -227,7 +227,17 @@ export const GraphQLSchema = { } if (field.selectable) { - // TODO + fields.selector.push({ + name: fieldName, + type: inputFieldType, + }); + } + + if (field.selectable && field.unique) { + fields.selectorUnique.push({ + name: fieldName, + type: inputFieldType, + }); } if (field.orderable) { @@ -289,13 +299,13 @@ export const GraphQLSchema = { const queryResolvers = {}; // single - if (resolvers.single) { + if (resolvers.single) { addGraphQLQuery(singleQueryTemplate({ typeName }), resolvers.single.description); queryResolvers[Utils.camelCaseify(typeName)] = resolvers.single.resolver.bind(resolvers.single); } // multi - if (resolvers.multi) { + if (resolvers.multi) { addGraphQLQuery(multiQueryTemplate({ typeName }), resolvers.multi.description); queryResolvers[Utils.camelCaseify(Utils.pluralize(typeName))] = resolvers.multi.resolver.bind(resolvers.multi); } diff --git a/packages/vulcan-lib/lib/modules/graphql_templates.js b/packages/vulcan-lib/lib/modules/graphql_templates.js index 64912d92e..f920a6cdb 100644 --- a/packages/vulcan-lib/lib/modules/graphql_templates.js +++ b/packages/vulcan-lib/lib/modules/graphql_templates.js @@ -2,7 +2,7 @@ import { Utils } from './utils'; export const convertToGraphQL = (fields, indentation) => { return fields.length > 0 ? fields.map(f => fieldTemplate(f, indentation)).join(`\n`) : ''; -} +}; export const arrayToGraphQL = fields => fields.map(f => `${f.name}: ${f.type}`).join(', '); @@ -19,7 +19,7 @@ export const getArguments = args => { } else { return ''; } -} +}; /* ------------------------------------- Generic Field Template ------------------------------------- */ @@ -158,6 +158,8 @@ export const singleInputTemplate = ({ typeName }) => selector: ${typeName}SelectorUniqueInput # Whether to enable caching for this query enableCache: Boolean + # Return null instead of throwing MissingDocumentError + allowNull: Boolean }`; /* @@ -175,11 +177,11 @@ type MultiMovieInput { export const multiInputTemplate = ({ typeName }) => `input Multi${typeName}Input { # A JSON object that contains the query terms used to fetch data - terms: JSON, + terms: JSON, # How much to offset the results by - offset: Int, + offset: Int, # A limit for the query - limit: Int, + limit: Int, # Whether to enable caching for this query enableCache: Boolean # Whether to calculate totalCount for this query @@ -348,7 +350,7 @@ export const createInputTemplate = ({ typeName }) => /* -Type for update mutation input argument +Type for update mutation input argument type UpdateMovieInput { selector: MovieSelectorUniqueInput! @@ -540,4 +542,4 @@ export const deleteClientTemplate = ({ typeName, fragmentName }) => ...${fragmentName} } } -}`; \ No newline at end of file +}`; diff --git a/packages/vulcan-users/lib/modules/permissions.js b/packages/vulcan-users/lib/modules/permissions.js index 0b44937d6..c8905d698 100644 --- a/packages/vulcan-users/lib/modules/permissions.js +++ b/packages/vulcan-users/lib/modules/permissions.js @@ -205,6 +205,7 @@ Users.isAdminById = Users.isAdmin; Users.getViewableFields = function (user, collection, document) { return Utils.arrayToFields(_.compact(_.map(collection.simpleSchema()._schema, (field, fieldName) => { + if (fieldName.indexOf('.$') > -1) return null; return Users.canReadField(user, field, document) ? fieldName : null; } )));