diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index aa637bc..58197ec 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -3,12 +3,7 @@ name: Test suite -on: - push: - branches: - - master - - develop - pull_request: +on: [push, pull_request] jobs: tests: @@ -17,12 +12,12 @@ jobs: # needs: [lintcode,lintstyle,lintdocs] # we could add prior jobs for linting, if desired steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup meteor uses: meteorengineer/setup-meteor@v1 with: - meteor-release: '2.2' + meteor-release: '2.13.3' - name: cache dependencies uses: actions/cache@v1 @@ -32,3 +27,4 @@ jobs: restore-keys: | ${{ runner.os }}-node- - run: cd tests && meteor npm install && meteor npm run test + diff --git a/package/collection2/collection2.js b/package/collection2/collection2.js index 97073a9..213884f 100644 --- a/package/collection2/collection2.js +++ b/package/collection2/collection2.js @@ -1,27 +1,29 @@ -import { EventEmitter } from 'meteor/raix:eventemitter'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -import { EJSON } from 'meteor/ejson'; -import isEmpty from 'lodash.isempty'; -import isEqual from 'lodash.isequal'; -import isObject from 'lodash.isobject'; -import { flattenSelector } from './lib'; +import { EventEmitter } from 'meteor/raix:eventemitter' +import { Meteor } from 'meteor/meteor' +import { Mongo } from 'meteor/mongo' +import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions' +import { EJSON } from 'meteor/ejson' +import isEmpty from 'lodash.isempty' +import isEqual from 'lodash.isequal' +import isObject from 'lodash.isobject' +import { flattenSelector, isInsertType, isUpdateType, isUpsertType } from './lib' -checkNpmVersions({ 'simpl-schema': '>=0.0.0' }, 'aldeed:collection2'); +/* global LocalCollection, Package */ -const SimpleSchema = require('simpl-schema').default; +checkNpmVersions({ 'simpl-schema': '>=0.0.0' }, 'aldeed:collection2') + +const SimpleSchema = require('simpl-schema').default // Exported only for listening to events -const Collection2 = new EventEmitter(); +const Collection2 = new EventEmitter() Collection2.cleanOptions = { filter: true, autoConvert: true, removeEmptyStrings: true, trimStrings: true, - removeNullsFromArrays: false, -}; + removeNullsFromArrays: false +} /** * Mongo.Collection.prototype.attachSchema @@ -31,6 +33,7 @@ Collection2.cleanOptions = { * @param {Boolean} [options.transform=false] Set to `true` if your document must be passed * through the collection's transform to properly validate. * @param {Boolean} [options.replace=false] Set to `true` to replace any existing schema instead of combining + * @param {Object} [options.selector] * @return {undefined} * * Use this method to attach a schema to a collection created by another package, @@ -38,52 +41,52 @@ Collection2.cleanOptions = { * once for a single collection, or to call this for a collection that had a * schema object passed to its constructor. */ -Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { - options = options || {}; +Mongo.Collection.prototype.attachSchema = function c2AttachSchema (ss, options) { + options = options || {} // Allow passing just the schema object if (!SimpleSchema.isSimpleSchema(ss)) { - ss = new SimpleSchema(ss); + ss = new SimpleSchema(ss) } - function attachTo(obj) { + function attachTo (obj) { // we need an array to hold multiple schemas // position 0 is reserved for the "base" schema - obj._c2 = obj._c2 || {}; - obj._c2._simpleSchemas = obj._c2._simpleSchemas || [ null ]; + obj._c2 = obj._c2 || {} + obj._c2._simpleSchemas = obj._c2._simpleSchemas || [null] - if (typeof options.selector === "object") { + if (typeof options.selector === 'object') { // Selector Schemas // Extend selector schema with base schema - const baseSchema = obj._c2._simpleSchemas[0]; + const baseSchema = obj._c2._simpleSchemas[0] if (baseSchema) { - ss = extendSchema(baseSchema.schema, ss); + ss = extendSchema(baseSchema.schema, ss) } // Index of existing schema with identical selector - let schemaIndex; + let schemaIndex // Loop through existing schemas with selectors, - for (schemaIndex = obj._c2._simpleSchemas.length - 1; 0 < schemaIndex; schemaIndex--) { - const schema = obj._c2._simpleSchemas[schemaIndex]; - if (schema && isEqual(schema.selector, options.selector)) break; + for (schemaIndex = obj._c2._simpleSchemas.length - 1; schemaIndex > 0; schemaIndex--) { + const schema = obj._c2._simpleSchemas[schemaIndex] + if (schema && isEqual(schema.selector, options.selector)) break } if (schemaIndex <= 0) { // We didn't find the schema in our array - push it into the array obj._c2._simpleSchemas.push({ schema: ss, - selector: options.selector, - }); + selector: options.selector + }) } else { // We found a schema with an identical selector in our array, if (options.replace === true) { // Replace existing selector schema with new selector schema - obj._c2._simpleSchemas[schemaIndex].schema = ss; + obj._c2._simpleSchemas[schemaIndex].schema = ss } else { // Extend existing selector schema with new selector schema. - obj._c2._simpleSchemas[schemaIndex].schema = extendSchema(obj._c2._simpleSchemas[schemaIndex].schema, ss); + obj._c2._simpleSchemas[schemaIndex].schema = extendSchema(obj._c2._simpleSchemas[schemaIndex].schema, ss) } } } else { @@ -92,41 +95,42 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { // Replace base schema and delete all other schemas obj._c2._simpleSchemas = [{ schema: ss, - selector: options.selector, - }]; + selector: options.selector + }] } else { // Set base schema if not yet set if (!obj._c2._simpleSchemas[0]) { - return obj._c2._simpleSchemas[0] = { schema: ss, selector: undefined }; + obj._c2._simpleSchemas[0] = { schema: ss, selector: undefined } + return obj._c2._simpleSchemas[0] } // Extend base schema and therefore extend all schemas obj._c2._simpleSchemas.forEach((schema, index) => { if (obj._c2._simpleSchemas[index]) { - obj._c2._simpleSchemas[index].schema = extendSchema(obj._c2._simpleSchemas[index].schema, ss); + obj._c2._simpleSchemas[index].schema = extendSchema(obj._c2._simpleSchemas[index].schema, ss) } - }); + }) } } } - attachTo(this); + attachTo(this) // Attach the schema to the underlying LocalCollection, too if (this._collection instanceof LocalCollection) { - this._collection._c2 = this._collection._c2 || {}; - attachTo(this._collection); + this._collection._c2 = this._collection._c2 || {} + attachTo(this._collection) } - defineDeny(this, options); - keepInsecure(this); + defineDeny(this, options) + keepInsecure(this) - Collection2.emit('schema.attached', this, ss, options); + Collection2.emit('schema.attached', this, ss, options) }; [Mongo.Collection, LocalCollection].forEach((obj) => { /** * simpleSchema * @description function detect the correct schema by given params. If it - * detect multi-schema presence in the collection, then it made an attempt to find a + * detects multi-schema presence in the collection, then it made an attempt to find a * `selector` in args * @param {Object} doc - It could be on update/upsert or document * itself on insert/remove @@ -135,261 +139,344 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { * @return {Object} Schema */ obj.prototype.simpleSchema = function (doc, options, query) { - if (!this._c2) return null; - if (this._c2._simpleSchema) return this._c2._simpleSchema; + if (!this._c2) return null + if (this._c2._simpleSchema) return this._c2._simpleSchema - const schemas = this._c2._simpleSchemas; + const schemas = this._c2._simpleSchemas if (schemas && schemas.length > 0) { - - let schema, selector, target; + let schema, selector, target // Position 0 reserved for base schema - for (var i = 1; i < schemas.length; i++) { - schema = schemas[i]; - selector = Object.keys(schema.selector)[0]; + for (let i = 1; i < schemas.length; i++) { + schema = schemas[i] + selector = Object.keys(schema.selector)[0] - // We will set this to undefined because in theory you might want to select + // We will set this to undefined because in theory, you might want to select // on a null value. - target = undefined; + target = undefined // here we are looking for selector in different places // $set should have more priority here if (doc.$set && typeof doc.$set[selector] !== 'undefined') { - target = doc.$set[selector]; + target = doc.$set[selector] } else if (typeof doc[selector] !== 'undefined') { - target = doc[selector]; + target = doc[selector] } else if (options && options.selector) { - target = options.selector[selector]; + target = options.selector[selector] } else if (query && query[selector]) { // on upsert/update operations - target = query[selector]; + target = query[selector] } // we need to compare given selector with doc property or option to - // find right schema + // find the right schema if (target !== undefined && target === schema.selector[selector]) { - return schema.schema; + return schema.schema } } if (schemas[0]) { - return schemas[0].schema; + return schemas[0].schema } else { - throw new Error("No default schema"); + throw new Error('No default schema') } } - return null; - }; -}); + return null + } +}) + +function _methodMutation (async, methodName) { + const _super = Meteor.isFibersDisabled + ? Mongo.Collection.prototype[methodName] + : Mongo.Collection.prototype[methodName.replace('Async', '')] -// Wrap DB write operation methods -['insert', 'update'].forEach((methodName) => { - const _super = Mongo.Collection.prototype[methodName]; - Mongo.Collection.prototype[methodName] = function(...args) { - let options = (methodName === "insert") ? args[1] : args[2]; + if (!_super) return + + Mongo.Collection.prototype[methodName] = function (...args) { + let options = (isInsertType(methodName)) ? args[1] : args[2] // Support missing options arg - if (!options || typeof options === "function") { - options = {}; + if (!options || typeof options === 'function') { + options = {} } + let validationContext = {} + let error if (this._c2 && options.bypassCollection2 !== true) { - let userId = null; + let userId = null try { // https://github.com/aldeed/meteor-collection2/issues/175 - userId = Meteor.userId(); - } catch (err) {} + userId = Meteor.userId() + } catch (err) { + } - args = doValidate( + [args, validationContext] = doValidate( this, methodName, args, Meteor.isServer || this._connection === null, // getAutoValues userId, - Meteor.isServer // isFromTrustedCode - ); + Meteor.isServer, // isFromTrustedCode + async + ) + if (!args) { - // doValidate already called the callback or threw the error so we're done. + // doValidate already called the callback or threw the error, so we're done. // But insert should always return an ID to match core behavior. - return methodName === "insert" ? this._makeNewID() : undefined; + return isInsertType(methodName) ? this._makeNewID() : undefined } } else { // We still need to adjust args because insert does not take options - if (methodName === "insert" && typeof args[1] !== 'function') args.splice(1, 1); + if (isInsertType(methodName) && typeof args[1] !== 'function') args.splice(1, 1) + } + + if (async && !Meteor.isFibersDisabled) { + try { + this[methodName.replace('Async', '')].isCalledFromAsync = true + _super.isCalledFromAsync = true + return Promise.resolve(_super.apply(this, args)) + } catch (err) { + const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') + ? 'addValidationErrors' + : 'addInvalidKeys' + parsingServerError([err], validationContext, addValidationErrorsPropName) + error = getErrorObject(validationContext, err.message) + return Promise.reject(error) + } + } else { + return _super.apply(this, args) + } + } +} + +function _methodMutationAsync (methodName) { + const _super = Mongo.Collection.prototype[methodName] + Mongo.Collection.prototype[methodName] = async function (...args) { + let options = (isInsertType(methodName)) ? args[1] : args[2] + + // Support missing options arg + if (!options || typeof options === 'function') { + options = {} } - return _super.apply(this, args); - }; -}); + let validationContext = {} + if (this._c2 && options.bypassCollection2 !== true) { + let userId = null + try { // https://github.com/aldeed/meteor-collection2/issues/175 + userId = Meteor.userId() + } catch (err) { + } + + [args, validationContext] = doValidate( + this, + methodName, + args, + Meteor.isServer || this._connection === null, // getAutoValues + userId, + Meteor.isServer, // isFromTrustedCode + true + ) + + if (!args) { + // doValidate already called the callback or threw the error, so we're done. + // But insert should always return an ID to match core behavior. + return isInsertType(methodName) ? this._makeNewID() : undefined + } + } else { + // We still need to adjust args because insert does not take options + if (methodName === 'insert' && typeof args[1] !== 'function') args.splice(1, 1) + } + + try { + return await _super.apply(this, args) + } catch (err) { + const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') + ? 'addValidationErrors' + : 'addInvalidKeys' + parsingServerError([err], validationContext, addValidationErrorsPropName) + throw getErrorObject(validationContext, err.message) + } + } +} + +// Wrap DB write operation methods +if (Mongo.Collection.prototype.insertAsync) { + if (Meteor.isFibersDisabled) { + ['insertAsync', 'updateAsync'].forEach(_methodMutationAsync.bind(this)) + } else { + ['insertAsync', 'updateAsync'].forEach(_methodMutation.bind(this, true)) + } +} +['insert', 'update'].forEach(_methodMutation.bind(this, false)) /* * Private */ -function doValidate(collection, type, args, getAutoValues, userId, isFromTrustedCode) { - let doc, callback, error, options, isUpsert, selector, last, hasCallback; +function doValidate (collection, type, args, getAutoValues, userId, isFromTrustedCode, async) { + let doc, callback, error, options, selector if (!args.length) { - throw new Error(type + " requires an argument"); + throw new Error(type + ' requires an argument') } // Gather arguments and cache the selector - if (type === "insert") { - doc = args[0]; - options = args[1]; - callback = args[2]; + if (isInsertType(type)) { + doc = args[0] + options = args[1] + callback = args[2] // The real insert doesn't take options - if (typeof options === "function") { - args = [doc, options]; - } else if (typeof callback === "function") { - args = [doc, callback]; + if (typeof options === 'function') { + args = [doc, options] + } else if (typeof callback === 'function') { + args = [doc, callback] } else { - args = [doc]; + args = [doc] } - } else if (type === "update") { - selector = args[0]; - doc = args[1]; - options = args[2]; - callback = args[3]; + } else if (isUpdateType(type)) { + selector = args[0] + doc = args[1] + options = args[2] + callback = args[3] } else { - throw new Error("invalid type argument"); + throw new Error('invalid type argument') } - const validatedObjectWasInitiallyEmpty = isEmpty(doc); + const validatedObjectWasInitiallyEmpty = isEmpty(doc) // Support missing options arg - if (!callback && typeof options === "function") { - callback = options; - options = {}; + if (!callback && typeof options === 'function') { + callback = options + options = {} } - options = options || {}; + options = options || {} - last = args.length - 1; + const last = args.length - 1 - hasCallback = (typeof args[last] === 'function'); + const hasCallback = (typeof args[last] === 'function') // If update was called with upsert:true, flag as an upsert - isUpsert = (type === "update" && options.upsert === true); + const isUpsert = (isUpdateType(type) && options.upsert === true) // we need to pass `doc` and `options` to `simpleSchema` method, that's why // schema declaration moved here - let schema = collection.simpleSchema(doc, options, selector); - const isLocalCollection = (collection._connection === null); + let schema = collection.simpleSchema(doc, options, selector) + const isLocalCollection = (collection._connection === null) // On the server and for local collections, we allow passing `getAutoValues: false` to disable autoValue functions if ((Meteor.isServer || isLocalCollection) && options.getAutoValues === false) { - getAutoValues = false; + getAutoValues = false } // Process pick/omit options if they are present - const picks = Array.isArray(options.pick) ? options.pick : null; - const omits = Array.isArray(options.omit) ? options.omit : null; + const picks = Array.isArray(options.pick) ? options.pick : null + const omits = Array.isArray(options.omit) ? options.omit : null if (picks && omits) { // Pick and omit cannot both be present in the options - throw new Error('pick and omit options are mutually exclusive'); + throw new Error('pick and omit options are mutually exclusive') } else if (picks) { - schema = schema.pick(...picks); + schema = schema.pick(...picks) } else if (omits) { - schema = schema.omit(...omits); + schema = schema.omit(...omits) } // Determine validation context - let validationContext = options.validationContext; + let validationContext = options.validationContext if (validationContext) { if (typeof validationContext === 'string') { - validationContext = schema.namedContext(validationContext); + validationContext = schema.namedContext(validationContext) } } else { - validationContext = schema.namedContext(); + validationContext = schema.namedContext() } // Add a default callback function if we're on the client and no callback was given - if (Meteor.isClient && !callback) { + if (Meteor.isClient && !callback && !async) { // Client can't block, so it can't report errors by exception, // only by callback. If they forget the callback, give them a // default one that logs the error, so they aren't totally - // baffled if their writes don't work because their database is + // baffled if their writing doesn't work because their database is // down. - callback = function(err) { + callback = function (err) { if (err) { - Meteor._debug(type + " failed: " + (err.reason || err.stack)); + Meteor._debug(type + ' failed: ' + (err.reason || err.stack)) } - }; + } } // If client validation is fine or is skipped but then something // is found to be invalid on the server, we get that error back // as a special Meteor.Error that we need to parse. if (Meteor.isClient && hasCallback) { - callback = args[last] = wrapCallbackForParsingServerErrors(validationContext, callback); + callback = args[last] = wrapCallbackForParsingServerErrors(validationContext, callback) } - const schemaAllowsId = schema.allowsKey("_id"); - if (type === "insert" && !doc._id && schemaAllowsId) { - doc._id = collection._makeNewID(); + const schemaAllowsId = schema.allowsKey('_id') + if (isInsertType(type) && !doc._id && schemaAllowsId) { + doc._id = collection._makeNewID() } // Get the docId for passing in the autoValue/custom context - let docId; - if (type === 'insert') { - docId = doc._id; // might be undefined - } else if (type === "update" && selector) { - docId = typeof selector === 'string' || selector instanceof Mongo.ObjectID ? selector : selector._id; + let docId + if (isInsertType(type)) { + docId = doc._id // might be undefined + } else if (isUpdateType(type) && selector) { + docId = typeof selector === 'string' || selector instanceof Mongo.ObjectID ? selector : selector._id } // If _id has already been added, remove it temporarily if it's // not explicitly defined in the schema. - let cachedId; + let cachedId if (doc._id && !schemaAllowsId) { - cachedId = doc._id; - delete doc._id; + cachedId = doc._id + delete doc._id } const autoValueContext = { - isInsert: (type === "insert"), - isUpdate: (type === "update" && options.upsert !== true), + isInsert: isInsertType(type), + isUpdate: isUpdateType(type) && options.upsert !== true, isUpsert, userId, isFromTrustedCode, docId, isLocalCollection - }; + } const extendAutoValueContext = { ...((schema._cleanOptions || {}).extendAutoValueContext || {}), ...autoValueContext, - ...options.extendAutoValueContext, - }; + ...options.extendAutoValueContext + } const cleanOptionsForThisOperation = {}; - ["autoConvert", "filter", "removeEmptyStrings", "removeNullsFromArrays", "trimStrings"].forEach(prop => { - if (typeof options[prop] === "boolean") { - cleanOptionsForThisOperation[prop] = options[prop]; + ['autoConvert', 'filter', 'removeEmptyStrings', 'removeNullsFromArrays', 'trimStrings'].forEach(prop => { + if (typeof options[prop] === 'boolean') { + cleanOptionsForThisOperation[prop] = options[prop] } - }); + }) // Preliminary cleaning on both client and server. On the server and for local // collections, automatic values will also be set at this point. schema.clean(doc, { mutate: true, // Clean the doc/modifier in place - isModifier: (type !== "insert"), + isModifier: !isInsertType(type), // Start with some Collection2 defaults, which will usually be overwritten ...Collection2.cleanOptions, - // The extend with the schema-level defaults (from SimpleSchema constructor options) + // The extent with the schema-level defaults (from SimpleSchema constructor options) ...(schema._cleanOptions || {}), // Finally, options for this specific operation should take precedence ...cleanOptionsForThisOperation, extendAutoValueContext, // This was extended separately above - getAutoValues, // Force this override - }); + getAutoValues // Force this override + }) - // We clone before validating because in some cases we need to adjust the + // We clone before validating because in some cases, we need to adjust the // object a bit before validating it. If we adjusted `doc` itself, our // changes would persist into the database. - let docToValidate = {}; - for (var prop in doc) { + const docToValidate = {} + for (const prop in doc) { // We omit prototype properties when cloning because they will not be valid // and mongo omits them when saving to the database anyway. if (Object.prototype.hasOwnProperty.call(doc, prop)) { - docToValidate[prop] = doc[prop]; + docToValidate[prop] = doc[prop] } } @@ -402,11 +489,11 @@ function doValidate(collection, type, args, getAutoValues, userId, isFromTrusted // This is no doubt prone to errors, but there probably isn't any better way // right now. if (Meteor.isServer && isUpsert && isObject(selector)) { - const set = docToValidate.$set || {}; - docToValidate.$set = flattenSelector(selector); + const set = docToValidate.$set || {} + docToValidate.$set = flattenSelector(selector) - if (!schemaAllowsId) delete docToValidate.$set._id; - Object.assign(docToValidate.$set, set); + if (!schemaAllowsId) delete docToValidate.$set._id + Object.assign(docToValidate.$set, set) } // Set automatic values for validation on the client. // On the server, we already updated doc with auto values, but on the client, @@ -418,172 +505,188 @@ function doValidate(collection, type, args, getAutoValues, userId, isFromTrusted extendAutoValueContext, filter: false, getAutoValues: true, - isModifier: (type !== "insert"), + isModifier: !isInsertType(type), mutate: true, // Clean the doc/modifier in place removeEmptyStrings: false, removeNullsFromArrays: false, - trimStrings: false, - }); + trimStrings: false + }) } // XXX Maybe move this into SimpleSchema if (!validatedObjectWasInitiallyEmpty && isEmpty(docToValidate)) { throw new Error('After filtering out keys not in the schema, your ' + - (type === 'update' ? 'modifier' : 'object') + - ' is now empty'); + (isUpdateType(type) ? 'modifier' : 'object') + + ' is now empty') } // Validate doc - let isValid; + let isValid if (options.validate === false) { - isValid = true; + isValid = true } else { isValid = validationContext.validate(docToValidate, { - modifier: (type === "update" || type === "upsert"), + modifier: (isUpdateType(type) || isUpsertType(type)), upsert: isUpsert, extendedCustomContext: { - isInsert: (type === "insert"), - isUpdate: (type === "update" && options.upsert !== true), + isInsert: isInsertType(type), + isUpdate: isUpdateType(type) && options.upsert !== true, isUpsert, userId, isFromTrustedCode, docId, isLocalCollection, - ...(options.extendedCustomContext || {}), - }, - }); + ...(options.extendedCustomContext || {}) + } + }) } if (isValid) { // Add the ID back if (cachedId) { - doc._id = cachedId; + doc._id = cachedId } // Update the args to reflect the cleaned doc - // XXX not sure this is necessary since we mutate - if (type === "insert") { - args[0] = doc; + // XXX not sure if this is necessary since we mutate + if (isInsertType(type)) { + args[0] = doc } else { - args[1] = doc; + args[1] = doc } // If callback, set invalidKey when we get a mongo unique error if (Meteor.isServer && hasCallback) { - args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]); + args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]) } - return args; + return [args, validationContext] } else { - error = getErrorObject(validationContext, Meteor.settings?.packages?.collection2?.disableCollectionNamesInValidation ? '' : `in ${collection._name} ${type}`); + error = getErrorObject(validationContext, Meteor.settings?.packages?.collection2?.disableCollectionNamesInValidation ? '' : `in ${collection._name} ${type}`) if (callback) { // insert/update/upsert pass `false` when there's an error, so we do that - callback(error, false); + callback(error, false) + return [] } else { - throw error; + throw error } } } -function getErrorObject(context, appendToMessage = '') { - let message; - const invalidKeys = (typeof context.validationErrors === 'function') ? context.validationErrors() : context.invalidKeys(); +function getErrorObject (context, appendToMessage = '') { + let message + const invalidKeys = (typeof context.validationErrors === 'function') ? context.validationErrors() : context.invalidKeys() if (invalidKeys.length) { - const firstErrorKey = invalidKeys[0].name; - const firstErrorMessage = context.keyErrorMessage(firstErrorKey); + const firstErrorKey = invalidKeys[0].name + const firstErrorMessage = context.keyErrorMessage(firstErrorKey) // If the error is in a nested key, add the full key to the error message // to be more helpful. if (firstErrorKey.indexOf('.') === -1) { - message = firstErrorMessage; + message = firstErrorMessage } else { - message = `${firstErrorMessage} (${firstErrorKey})`; + message = `${firstErrorMessage} (${firstErrorKey})` } } else { - message = "Failed validation"; + message = 'Failed validation' } - message = `${message} ${appendToMessage}`.trim(); - const error = new Error(message); - error.invalidKeys = invalidKeys; - error.validationContext = context; + message = `${message} ${appendToMessage}`.trim() + const error = new Error(message) + error.invalidKeys = invalidKeys + error.validationContext = context // If on the server, we add a sanitized error, too, in case we're // called from a method. if (Meteor.isServer) { - error.sanitizedError = new Meteor.Error(400, message, EJSON.stringify(error.invalidKeys)); + error.sanitizedError = new Meteor.Error(400, message, EJSON.stringify(error.invalidKeys)) } - return error; + return error } -function addUniqueError(context, errorMessage) { - const name = errorMessage.split('c2_')[1].split(' ')[0]; - const val = errorMessage.split('dup key:')[1].split('"')[1]; +function addUniqueError (context, errorMessage) { + const name = errorMessage.split('c2_')[1].split(' ')[0] + const val = errorMessage.split('dup key:')[1].split('"')[1] - const addValidationErrorsPropName = (typeof context.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys'; + const addValidationErrorsPropName = (typeof context.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys' context[addValidationErrorsPropName]([{ - name: name, + name, type: 'notUnique', value: val - }]); + }]) } -function wrapCallbackForParsingMongoValidationErrors(validationContext, cb) { - return function wrappedCallbackForParsingMongoValidationErrors(...args) { - const error = args[0]; +function parsingServerError (args, validationContext, addValidationErrorsPropName) { + const error = args[0] + // Handle our own validation errors + if (error instanceof Meteor.Error && + error.error === 400 && + error.reason === 'INVALID' && + typeof error.details === 'string') { + const invalidKeysFromServer = EJSON.parse(error.details) + validationContext[addValidationErrorsPropName](invalidKeysFromServer) + args[0] = getErrorObject(validationContext) + } else if (error instanceof Meteor.Error && + // Handle Mongo unique index errors, which are forwarded to the client as 409 errors + error.error === 409 && + error.reason && + error.reason.indexOf('E11000') !== -1 && + error.reason.indexOf('c2_') !== -1) { + addUniqueError(validationContext, error.reason) + args[0] = getErrorObject(validationContext) + } +} + +function wrapCallbackForParsingMongoValidationErrors (validationContext, cb) { + return function wrappedCallbackForParsingMongoValidationErrors (...args) { + const error = args[0] if (error && - ((error.name === "MongoError" && error.code === 11001) || error.message.indexOf('MongoError: E11000') !== -1) && - error.message.indexOf('c2_') !== -1) { - addUniqueError(validationContext, error.message); - args[0] = getErrorObject(validationContext); + ((error.name === 'MongoError' && error.code === 11001) || error.message.indexOf('MongoError: E11000') !== -1) && + error.message.indexOf('c2_') !== -1) { + addUniqueError(validationContext, error.message) + args[0] = getErrorObject(validationContext) } - return cb.apply(this, args); - }; + return cb.apply(this, args) + } } -function wrapCallbackForParsingServerErrors(validationContext, cb) { - const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys'; - return function wrappedCallbackForParsingServerErrors(...args) { - const error = args[0]; - // Handle our own validation errors - if (error instanceof Meteor.Error && - error.error === 400 && - error.reason === "INVALID" && - typeof error.details === "string") { - const invalidKeysFromServer = EJSON.parse(error.details); - validationContext[addValidationErrorsPropName](invalidKeysFromServer); - args[0] = getErrorObject(validationContext); - } - // Handle Mongo unique index errors, which are forwarded to the client as 409 errors - else if (error instanceof Meteor.Error && - error.error === 409 && - error.reason && - error.reason.indexOf('E11000') !== -1 && - error.reason.indexOf('c2_') !== -1) { - addUniqueError(validationContext, error.reason); - args[0] = getErrorObject(validationContext); - } - return cb.apply(this, args); - }; +function wrapCallbackForParsingServerErrors (validationContext, cb) { + const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys' + return function wrappedCallbackForParsingServerErrors (...args) { + parsingServerError(args, validationContext, addValidationErrorsPropName) + return cb.apply(this, args) + } } -let alreadyInsecure = {}; -function keepInsecure(c) { +const alreadyInsecure = {} + +function keepInsecure (c) { // If insecure package is in use, we need to add allow rules that return // true. Otherwise, it would seemingly turn off insecure mode. if (Package && Package.insecure && !alreadyInsecure[c._name]) { - c.allow({ - insert: function() { - return true; + const allow = { + insert: function () { + return true }, - update: function() { - return true; + update: function () { + return true }, remove: function () { - return true; + return true }, fetch: [], transform: null - }); - alreadyInsecure[c._name] = true; + } + + if (Meteor.isFibersDisabled) { + Object.assign(allow, { + insertAsync: allow.insert, + updateAsync: allow.update, + removeAsync: allow.remove + }) + } + + c.allow(allow) + + alreadyInsecure[c._name] = true } // If insecure package is NOT in use, then adding the two deny functions // does not have any effect on the main app's security paradigm. The @@ -592,17 +695,17 @@ function keepInsecure(c) { // additional deny functions, but does not have to. } -let alreadyDefined = {}; -function defineDeny(c, options) { - if (!alreadyDefined[c._name]) { +const alreadyDefined = {} - const isLocalCollection = (c._connection === null); +function defineDeny (c, options) { + if (!alreadyDefined[c._name]) { + const isLocalCollection = (c._connection === null) - // First define deny functions to extend doc with the results of clean + // First, define deny functions to extend doc with the results of clean // and auto-values. This must be done with "transform: null" or we would be // extending a clone of doc and therefore have no effect. - c.deny({ - insert: function(userId, doc) { + const firstDeny = { + insert: function (userId, doc) { // Referenced doc is cleaned in place c.simpleSchema(doc).clean(doc, { mutate: true, @@ -616,16 +719,16 @@ function defineDeny(c, options) { isInsert: true, isUpdate: false, isUpsert: false, - userId: userId, + userId, isFromTrustedCode: false, docId: doc._id, - isLocalCollection: isLocalCollection + isLocalCollection } - }); + }) - return false; + return false }, - update: function(userId, doc, fields, modifier) { + update: function (userId, doc, fields, modifier) { // Referenced modifier is cleaned in place c.simpleSchema(modifier).clean(modifier, { mutate: true, @@ -639,31 +742,40 @@ function defineDeny(c, options) { isInsert: false, isUpdate: true, isUpsert: false, - userId: userId, + userId, isFromTrustedCode: false, docId: doc && doc._id, - isLocalCollection: isLocalCollection + isLocalCollection } - }); + }) - return false; + return false }, fetch: ['_id'], transform: null - }); + } + + if (Meteor.isFibersDisabled) { + Object.assign(firstDeny, { + insertAsync: firstDeny.insert, + updateAsync: firstDeny.update + }) + } + + c.deny(firstDeny) - // Second define deny functions to validate again on the server + // Second, define deny functions to validate again on the server // for client-initiated inserts and updates. These should be // called after the clean/auto-value functions since we're adding // them after. These must *not* have "transform: null" if options.transform is true because // we need to pass the doc through any transforms to be sure // that custom types are properly recognized for type validation. - c.deny({ - insert: function(userId, doc) { - // We pass the false options because we will have done them on client if desired + const secondDeny = { + insert: function (userId, doc) { + // We pass the false options because we will have done them on the client if desired doValidate( c, - "insert", + 'insert', [ doc, { @@ -672,28 +784,28 @@ function defineDeny(c, options) { filter: false, autoConvert: false }, - function(error) { + function (error) { if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)) } } ], false, // getAutoValues userId, false // isFromTrustedCode - ); + ) - return false; + return false }, - update: function(userId, doc, fields, modifier) { + update: function (userId, doc, fields, modifier) { // NOTE: This will never be an upsert because client-side upserts // are not allowed once you define allow/deny functions. - // We pass the false options because we will have done them on client if desired + // We pass the false options because we will have done them on the client if desired doValidate( c, - "update", + 'update', [ - {_id: doc && doc._id}, + { _id: doc && doc._id }, modifier, { trimStrings: false, @@ -701,37 +813,46 @@ function defineDeny(c, options) { filter: false, autoConvert: false }, - function(error) { + function (error) { if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)) } } ], false, // getAutoValues userId, false // isFromTrustedCode - ); + ) - return false; + return false }, fetch: ['_id'], - ...(options.transform === true ? {} : {transform: null}), - }); + ...(options.transform === true ? {} : { transform: null }) + } + + if (Meteor.isFibersDisabled) { + Object.assign(secondDeny, { + insertAsync: secondDeny.insert, + updateAsync: secondDeny.update + }) + } + + c.deny(secondDeny) // note that we've already done this collection so that we don't do it again // if attachSchema is called again - alreadyDefined[c._name] = true; + alreadyDefined[c._name] = true } } -function extendSchema(s1, s2) { +function extendSchema (s1, s2) { if (s2.version >= 2) { - const ss = new SimpleSchema(s1); - ss.extend(s2); - return ss; + const ss = new SimpleSchema(s1) + ss.extend(s2) + return ss } else { - return new SimpleSchema([ s1, s2 ]); + return new SimpleSchema([s1, s2]) } } -export default Collection2; +export default Collection2 diff --git a/package/collection2/lib.js b/package/collection2/lib.js index 057195d..ad617b4 100644 --- a/package/collection2/lib.js +++ b/package/collection2/lib.js @@ -1,9 +1,9 @@ -export function flattenSelector(selector) { - // If selector uses $and format, convert to plain object selector +export function flattenSelector (selector) { + // If the selector uses $and format, convert to plain object selector if (Array.isArray(selector.$and)) { selector.$and.forEach(sel => { - Object.assign(selector, flattenSelector(sel)); - }); + Object.assign(selector, flattenSelector(sel)) + }) delete selector.$and } @@ -12,13 +12,13 @@ export function flattenSelector(selector) { Object.entries(selector).forEach(([key, value]) => { // Ignoring logical selectors (https://docs.mongodb.com/manual/reference/operator/query/#logical) - if (!key.startsWith("$")) { + if (!key.startsWith('$')) { if (typeof value === 'object' && value !== null) { if (value.$eq !== undefined) { obj[key] = value.$eq } else if (Array.isArray(value.$in) && value.$in.length === 1) { obj[key] = value.$in[0] - } else if (Object.keys(value).every(v => !(typeof v === "string" && v.startsWith("$")))) { + } else if (Object.keys(value).every(v => !(typeof v === 'string' && v.startsWith('$')))) { obj[key] = value } } else { @@ -26,6 +26,16 @@ export function flattenSelector(selector) { } } }) - + return obj } + +export const isInsertType = function (type) { + return ['insert', 'insertAsync'].includes(type) +} +export const isUpdateType = function (type) { + return ['update', 'updateAsync'].includes(type) +} +export const isUpsertType = function (type) { + return ['upsert', 'upsertAsync'].includes(type) +} diff --git a/package/collection2/package.js b/package/collection2/package.js index 4177ad1..16f5032 100644 --- a/package/collection2/package.js +++ b/package/collection2/package.js @@ -1,33 +1,33 @@ -/* global Package */ +/* global Package, Npm */ Package.describe({ - name: "aldeed:collection2", - summary: "Automatic validation of Meteor Mongo insert and update operations on the client and server", - version: "3.5.0", - documentation: "../../README.md", - git: "https://github.com/aldeed/meteor-collection2.git" -}); + name: 'aldeed:collection2', + summary: 'Automatic validation of Meteor Mongo insert and update operations on the client and server', + version: '3.6.0', + documentation: '../../README.md', + git: 'https://github.com/aldeed/meteor-collection2.git' +}) Npm.depends({ 'lodash.isempty': '4.4.0', 'lodash.isequal': '4.5.0', - 'lodash.isobject': '3.0.2', -}); + 'lodash.isobject': '3.0.2' +}) -Package.onUse(function(api) { - api.versionsFrom(['1.12.1', '2.3']); - api.use('mongo'); - api.imply('mongo'); - api.use('minimongo'); - api.use('ejson'); - api.use('raix:eventemitter@1.0.0'); - api.use('ecmascript'); - api.use('tmeasday:check-npm-versions@1.0.2'); +Package.onUse(function (api) { + api.versionsFrom(['1.12.1', '2.3', '3.0-alpha.15']) + api.use('mongo') + api.imply('mongo') + api.use('minimongo') + api.use('ejson') + api.use('raix:eventemitter') + api.use('ecmascript') + api.use('tmeasday:check-npm-versions') // Allow us to detect 'insecure'. - api.use('insecure@1.0.7', {weak: true}); + api.use('insecure', { weak: true }) - api.mainModule('collection2.js'); + api.mainModule('collection2.js') - api.export('Collection2'); -}); + api.export('Collection2') +}) diff --git a/tests/.meteor/packages b/tests/.meteor/packages index 7e11127..8f22c13 100644 --- a/tests/.meteor/packages +++ b/tests/.meteor/packages @@ -4,24 +4,22 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -meteor-base@1.4.0 # Packages every Meteor app needs to have -mobile-experience@1.1.0 # Packages for a great mobile UX -mongo@1.11.0 # The database Meteor supports right now -blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views -reactive-var@1.0.11 # Reactive variable for tracker +meteor-base@1.5.1 # Packages every Meteor app needs to have +mongo@1.16.7 # The database Meteor supports right now +reactive-var@1.0.12 # Reactive variable for tracker jquery # Helpful client-side library -tracker@1.2.0 # Meteor's client-side reactive programming library +tracker@1.3.2 # Meteor's client-side reactive programming library -standard-minifier-css@1.7.2 # CSS minifier run for production mode -standard-minifier-js@2.6.0 # JS minifier run for production mode +standard-minifier-css@1.9.2 # CSS minifier run for production mode +standard-minifier-js@2.8.1 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.15.1 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.16.7 # Enable ECMAScript2015+ syntax in app code shell-server@0.5.0 # Server-side component of the `meteor shell` command autopublish@1.0.7 # Publish all data to the clients (for prototyping) insecure@1.0.7 # Allow all DB writes from clients (for prototyping) -underscore@1.0.10 -dynamic-import@0.6.0 +underscore@1.0.13 +dynamic-import@0.7.3 + aldeed:collection2 -meteortesting:mocha diff --git a/tests/.meteor/release b/tests/.meteor/release index d8fd7cf..6641d04 100644 --- a/tests/.meteor/release +++ b/tests/.meteor/release @@ -1 +1 @@ -METEOR@2.2 +METEOR@2.13.3 diff --git a/tests/.meteor/versions b/tests/.meteor/versions index c9986aa..8409622 100644 --- a/tests/.meteor/versions +++ b/tests/.meteor/versions @@ -1,87 +1,65 @@ -aldeed:collection2@3.4.1 -allow-deny@1.1.0 +aldeed:collection2@3.6.0 +allow-deny@1.1.1 autopublish@1.0.7 -autoupdate@1.7.0 -babel-compiler@7.6.1 -babel-runtime@1.5.0 +autoupdate@1.8.0 +babel-compiler@7.10.4 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.4.0 -blaze-html-templates@1.2.0 -blaze-tools@1.1.1 boilerplate-generator@1.7.1 -caching-compiler@1.2.2 -caching-html-compiler@1.2.0 -callback-hook@1.3.0 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.4.1 +callback-hook@1.5.1 +check@1.3.2 +ddp@1.4.1 +ddp-client@2.6.1 ddp-common@1.4.0 -ddp-server@2.3.3 -diff-sequence@1.1.1 -dynamic-import@0.6.0 -ecmascript@0.15.1 -ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.0 -ecmascript-runtime-server@0.10.0 -ejson@1.1.1 +ddp-server@2.6.2 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +ejson@1.1.3 es5-shim@4.8.0 -fetch@0.1.1 -geojson-utils@1.0.10 +fetch@0.1.3 +geojson-utils@1.0.11 hot-code-push@1.0.4 -html-tools@1.1.1 -htmljs@1.1.0 -http@1.4.4 +http@1.0.10 id-map@1.1.1 insecure@1.0.7 inter-process-messaging@0.1.1 jquery@3.0.0 -launch-screen@1.2.1 -livedata@1.0.18 -logging@1.2.0 -meteor@1.9.3 -meteor-base@1.4.0 -meteortesting:browser-tests@1.3.4 -meteortesting:mocha@2.0.1 -meteortesting:mocha-core@8.1.2 -minifier-css@1.5.4 -minifier-js@2.6.0 -minimongo@1.6.2 -mobile-experience@1.1.0 -mobile-status-bar@1.1.0 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.11.1 -mongo-decimal@0.1.2 +logging@1.3.2 +meteor@1.11.3 +meteor-base@1.5.1 +minifier-css@1.6.4 +minifier-js@2.7.5 +minimongo@1.9.3 +modern-browsers@0.1.9 +modules@0.19.0 +modules-runtime@0.13.1 +mongo@1.16.7 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@3.9.0 -observe-sequence@1.0.16 +npm-mongo@4.16.0 ordered-dict@1.1.0 -promise@0.11.2 +promise@0.12.2 raix:eventemitter@1.0.0 -random@1.2.0 -react-fast-refresh@0.1.1 -reactive-var@1.0.11 +random@1.2.1 +react-fast-refresh@0.2.7 +reactive-var@1.0.12 reload@1.3.1 retry@1.1.0 -routepolicy@1.1.0 +routepolicy@1.1.1 shell-server@0.5.0 -socket-stream-client@0.3.2 -spacebars@1.1.0 -spacebars-compiler@1.2.1 -standard-minifier-css@1.7.2 -standard-minifier-js@2.6.0 -templating@1.4.0 -templating-compiler@1.4.0 -templating-runtime@1.4.0 -templating-tools@1.2.0 -tmeasday:check-npm-versions@1.0.2 -tracker@1.2.0 -typescript@4.2.2 -ui@1.0.13 -underscore@1.0.10 -url@1.3.1 -webapp@1.10.1 -webapp-hashing@1.1.0 +socket-stream-client@0.5.1 +standard-minifier-css@1.9.2 +standard-minifier-js@2.8.1 +tmeasday:check-npm-versions@1.0.3 +tracker@1.3.2 +typescript@4.9.4 +underscore@1.0.13 +url@1.3.2 +webapp@1.13.5 +webapp-hashing@1.1.1 diff --git a/tests/_main.tests.js b/tests/_main.tests.js index b012711..414cde7 100644 --- a/tests/_main.tests.js +++ b/tests/_main.tests.js @@ -1 +1 @@ -import 'babel-polyfill'; +import 'babel-polyfill' diff --git a/tests/autoValue.tests.js b/tests/autoValue.tests.js index ca8ff95..74e52e7 100644 --- a/tests/autoValue.tests.js +++ b/tests/autoValue.tests.js @@ -1,9 +1,12 @@ -import { Meteor } from 'meteor/meteor'; -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import { Meteor } from 'meteor/meteor' +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { callMongoMethod } from './helper' -const collection = new Mongo.Collection('autoValueTestCollection'); +/* global describe, it */ + +const collection = new Mongo.Collection('autoValueTestCollection') const localCollection = new Mongo.Collection('autoValueTestLocalCollection', { connection: null }); [collection, localCollection].forEach((c) => { @@ -11,81 +14,93 @@ const localCollection = new Mongo.Collection('autoValueTestLocalCollection', { c clientAV: { type: SimpleSchema.Integer, optional: true, - autoValue() { - if (Meteor.isServer) return; - return (this.value || 0) + 1; + autoValue () { + if (Meteor.isServer) return + return (this.value || 0) + 1 } }, serverAV: { type: SimpleSchema.Integer, optional: true, - autoValue() { - if (Meteor.isClient) return; - return (this.value || 0) + 1; + autoValue () { + if (Meteor.isClient) return + return (this.value || 0) + 1 } } - })); -}); + })) +}) if (Meteor.isClient) { describe('autoValue on client', function () { it('for client insert, autoValues should be added on the server only (added to only a validated clone of the doc on client)', function (done) { collection.insert({}, (error, id) => { - const doc = collection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(1); - done(); - }); - }); + if (error) { + done(error) + return + } + const doc = collection.findOne(id) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(1) + done() + }) + }) it('runs function once for LocalCollection', function (done) { localCollection.insert({}, (error, id) => { - const doc = localCollection.findOne(id); - expect(doc.clientAV).toBe(1); - expect(doc.serverAV).toBe(undefined); - done(); - }); - }); + if (error) { + done(error) + return + } + const doc = localCollection.findOne(id) + expect(doc.clientAV).toBe(1) + expect(doc.serverAV).toBe(undefined) + done() + }) + }) it('with getAutoValues false, does not run function for LocalCollection', function (done) { localCollection.insert({}, { getAutoValues: false }, (error, id) => { - const doc = localCollection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(undefined); - done(); - }); - }); - }); + if (error) { + done(error) + return + } + const doc = localCollection.findOne(id) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(undefined) + done() + }) + }) + }) } if (Meteor.isServer) { describe('autoValue on server', function () { - it('runs function once', function () { - const id = collection.insert({}); - const doc = collection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(1); - }); + it('runs function once', async function () { + const id = await callMongoMethod(collection, 'insert', [{}]) + const doc = await callMongoMethod(collection, 'findOne', [id]) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(1) + }) - it('with getAutoValues false, does not run function', function () { - const id = collection.insert({}, { getAutoValues: false }); - const doc = collection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(undefined); - }); + it('with getAutoValues false, does not run function', async function () { + const id = await callMongoMethod(collection, 'insert', [{}, { getAutoValues: false }]) + const doc = await callMongoMethod(collection, 'findOne', [id]) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(undefined) + }) - it('runs function once for LocalCollection', function () { - const id = localCollection.insert({}); - const doc = localCollection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(1); - }); + it('runs function once for LocalCollection', async function () { + const id = await callMongoMethod(localCollection, 'insert', [{}]) + const doc = await callMongoMethod(localCollection, 'findOne', [id]) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(1) + }) - it('with getAutoValues false, does not run function for LocalCollection', function () { - const id = localCollection.insert({}, { getAutoValues: false }); - const doc = localCollection.findOne(id); - expect(doc.clientAV).toBe(undefined); - expect(doc.serverAV).toBe(undefined); - }); - }); + it('with getAutoValues false, does not run function for LocalCollection', async function () { + const id = await callMongoMethod(localCollection, 'insert', [{}, { getAutoValues: false }]) + const doc = await callMongoMethod(localCollection, 'findOne', [id]) + expect(doc.clientAV).toBe(undefined) + expect(doc.serverAV).toBe(undefined) + }) + }) } diff --git a/tests/books.tests.js b/tests/books.tests.js index 3971dd5..1c04c59 100644 --- a/tests/books.tests.js +++ b/tests/books.tests.js @@ -1,37 +1,42 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { Meteor } from 'meteor/meteor' +import { _ } from 'meteor/underscore' +import { callMeteorFetch, callMongoMethod } from './helper' + +/* global describe, it, beforeEach */ const booksSchema = new SimpleSchema({ title: { type: String, - label: "Title", - max: 200, + label: 'Title', + max: 200 }, author: { type: String, - label: "Author" + label: 'Author' }, copies: { type: SimpleSchema.Integer, - label: "Number of copies", + label: 'Number of copies', min: 0 }, lastCheckedOut: { type: Date, - label: "Last date this book was checked out", + label: 'Last date this book was checked out', optional: true }, summary: { type: String, - label: "Brief summary", + label: 'Brief summary', optional: true, max: 1000 }, isbn: { type: String, - label: "ISBN", - optional: true, + label: 'ISBN', + optional: true }, field1: { type: String, @@ -43,381 +48,579 @@ const booksSchema = new SimpleSchema({ }, createdAt: { type: Date, - optional: true, + optional: true }, updatedAt: { type: Date, - optional: true, + optional: true } -}); +}) -const books = new Mongo.Collection('books'); -books.attachSchema(booksSchema); +const books = new Mongo.Collection('books') +books.attachSchema(booksSchema) -const upsertTest = new Mongo.Collection('upsertTest'); +const upsertTest = new Mongo.Collection('upsertTest') upsertTest.attachSchema(new SimpleSchema({ - _id: {type: String}, - foo: {type: Number} -})); + _id: { type: String }, + foo: { type: Number } +})) -export default function addBooksTests() { +export default function addBooksTests () { describe('insert', function () { - beforeEach(function () { - books.find({}).forEach(book => { - books.remove(book._id); - }); - }); - - it('required', function (done) { - const maybeNext = _.after(2, done); - - const id = books.insert({ - title: "Ulysses", - author: "James Joyce" - }, (error, result) => { - //The insert will fail, error will be set, - expect(!!error).toBe(true); - //and result will be false because "copies" is required. - expect(result).toBe(false); - //The list of errors is available by calling books.simpleSchema().namedContext().validationErrors() - const validationErrors = books.simpleSchema().namedContext().validationErrors(); - expect(validationErrors.length).toBe(1); - - const key = validationErrors[0] || {}; - expect(key.name).toBe('copies'); - expect(key.type).toBe('required'); - maybeNext(); - }); - - expect(typeof id).toBe('string'); - maybeNext(); - }); + beforeEach(async function () { + for (const book of await callMeteorFetch(books, {})) { + await callMongoMethod(books, 'remove', [book._id]) + } + }) + + if (Meteor.isClient) { + it('required', function (done) { + const maybeNext = _.after(2, done) + + const id = books.insert({ + title: 'Ulysses', + author: 'James Joyce' + }, (error, result) => { + // The insert will fail, error will be set, + expect(!!error).toBe(true) + // and the result will be false because "copies" is required. + expect(result).toBe(false) + // The list of errors is available by calling books.simpleSchema().namedContext().validationErrors() + const validationErrors = books.simpleSchema().namedContext().validationErrors() + expect(validationErrors.length).toBe(1) + + const key = validationErrors[0] || {} + expect(key.name).toBe('copies') + expect(key.type).toBe('required') + maybeNext() + }) + + expect(typeof id).toBe('string') + maybeNext() + }) + + it('validate false', function (done) { + const title = 'Validate False Client' + + callMongoMethod(books, 'insert', [{ + title, + author: 'James Joyce' + }, { + validate: false, + validationContext: 'validateFalse' + }]) + .then(() => { + done(new Error('should not get here')) + }) + .catch(async (error) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse').validationErrors() + + // When validated: false on the client, + // we should still get a validation error and validationErrors back from the server + expect(!!error).toBe(true) + // There should be an `invalidKeys` property on the error, too + expect(error.invalidKeys.length).toBe(1) + // expect(!!result).toBe(false) + expect(validationErrors.length).toBe(1) + + const insertedBook = await callMongoMethod(books, 'findOne', [{ title }]) + expect(!!insertedBook).toBe(false) + + // do a good one to set up update test + callMongoMethod(books, 'insert', [{ + title: title + ' 2', + author: 'James Joyce', + copies: 1 + }, { + validate: false, + validationContext: 'validateFalse2' + }]) + .then(async (newId) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse2').validationErrors() + + expect(!!newId).toBe(true) + expect(validationErrors.length).toBe(0) + + const insertedBook = await callMongoMethod(books, 'findOne', [{ title: title + ' 2' }]) + expect(!!insertedBook).toBe(true) + + callMongoMethod(books, 'update', [{ + _id: newId + }, { + $set: { + copies: 'Yes Please' + } + }, { + validate: false, + validationContext: 'validateFalse3' + }]) + .then(() => { + done(new Error('should not get here')) + }) + .catch(async (error) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse3').validationErrors() + + // When validated: false on the client, + // we should still get a validation error and invalidKeys from the server + expect(!!error).toBe(true) + // There should be an `invalidKeys` property on the error, too + expect(error.invalidKeys.length).toBe(1) + // expect(!!result).toBe(false) + expect(validationErrors.length).toBe(1) + + const updatedBook = await callMongoMethod(books, 'findOne', [newId]) + expect(!!updatedBook).toBe(true) + // copies should still be 1 because our new value failed validation on the server + expect(updatedBook.copies).toBe(1) + + // now try a good one + callMongoMethod(books, 'update', [{ + _id: newId + }, { + $set: { + copies: 3 + } + }, { + validate: false, + validationContext: 'validateFalse4' + }]) + .then(async (result) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse4').validationErrors() + expect(result).toBe(1) + expect(validationErrors.length).toBe(0) + + const updatedBook = await callMongoMethod(books, 'findOne', [newId]) + expect(!!updatedBook).toBe(true) + // copies should be changed because we used a valid value + expect(updatedBook.copies).toBe(3) + done() + }) + }) + }) + }) + }) + } if (Meteor.isServer) { + it('required 1 on server', function (done) { + callMongoMethod(books, 'insert', [{ + title: 'Ulysses', + author: 'James Joyce' + }]) + .then((result) => { + done('should not get here') + }) + .catch((error) => { + // The insert will fail, error will be set, + expect(!!error).toBe(true) + // and result will be false because "copies" is required. + // TODO expect(result).toBe(false); + // The list of errors is available + // by calling books.simpleSchema().namedContext().validationErrors() + const validationErrors = books.simpleSchema().namedContext().validationErrors() + expect(validationErrors.length).toBe(1) + + const key = validationErrors[0] || {} + expect(key.name).toBe('copies') + expect(key.type).toBe('required') + done() + }) + }) + + it('required 2 on server', async function () { + const title = 'Validate False Server' + + let error + let newId + let result + // do a good one to set up update test + try { + newId = await callMongoMethod(books, 'insert', [{ + title: title + ' 2', + author: 'James Joyce', + copies: 1 + }, { + validate: false, + validationContext: 'validateFalse2' + }]) + } catch (e) { + error = e + } + + let validationErrors = books.simpleSchema().namedContext('validateFalse2').validationErrors() + + expect(!!error).toBe(false) + expect(!!newId).toBe(true) + expect(validationErrors.length).toBe(0) + + const insertedBook = await callMongoMethod(books, 'findOne', [{ title: title + ' 2' }]) + expect(!!insertedBook).toBe(true) + + try { + result = await callMongoMethod(books, 'update', [{ + _id: newId + }, { + $set: { + copies: 'Yes Please' + } + }, { + validate: false, + validationContext: 'validateFalse3' + }]) + error = null + } catch (e) { + error = e + } + + let updatedBook + validationErrors = books.simpleSchema().namedContext('validateFalse3').validationErrors() + + // When validated: false on the server, validation should be skipped + expect(!!error).toBe(false) + expect(!!result).toBe(true) + expect(validationErrors.length).toBe(0) + + updatedBook = await callMongoMethod(books, 'findOne', [newId]) + + expect(!!updatedBook).toBe(true) + // copies should be changed despite being invalid because we skipped validation on the server + expect(updatedBook.copies).toBe('Yes Please') + + // now try a good one + try { + result = await callMongoMethod(books, 'update', [{ + _id: newId + }, { + $set: { + copies: 3 + } + }, { + validate: false, + validationContext: 'validateFalse4' + }]) + error = null + } catch (e) { + error = e + } + + validationErrors = books.simpleSchema().namedContext('validateFalse4').validationErrors() + expect(!!error).toBe(false) + expect(result).toBe(1) + expect(validationErrors.length).toBe(0) + + updatedBook = await callMongoMethod(books, 'findOne', [newId]) + expect(!!updatedBook).toBe(true) + // copies should be changed because we used a valid value + expect(updatedBook.copies).toBe(3) + }) + it('no validation when calling underlying _collection on the server', function (done) { - books._collection.insert({ - title: "Ulysses", - author: "James Joyce", + callMongoMethod(books._collection, 'insert', [{ + title: 'Ulysses', + author: 'James Joyce', copies: 1, updatedAt: new Date() - }, (error) => { - expect(error).toBe(null); - done(); - }); - }); + }]) + .then(() => { done() }) + .catch(done) + }) } - }); + }) if (Meteor.isServer) { describe('upsert', function () { - function getCallback(done) { - return (error, result) => { - expect(!!error).toBe(false); - expect(result.numberAffected).toBe(1); + function getCallback (done) { + return (result) => { + expect(result.numberAffected).toBe(1) - const validationErrors = books.simpleSchema().namedContext().validationErrors(); - expect(validationErrors.length).toBe(0); + const validationErrors = books.simpleSchema().namedContext().validationErrors() + expect(validationErrors.length).toBe(0) - done(); - }; + done() + } } - function getUpdateCallback(done) { - return (error, result) => { - if (error) console.error(error); - expect(!!error).toBe(false); - expect(result).toBe(1); + function getUpdateCallback (done) { + return (result) => { + expect(result).toBe(1) - const validationErrors = books.simpleSchema().namedContext().validationErrors(); - expect(validationErrors.length).toBe(0); + const validationErrors = books.simpleSchema().namedContext().validationErrors() + expect(validationErrors.length).toBe(0) - done(); - }; + done() + } } - function getErrorCallback(done) { - return (error, result) => { - expect(!!error).toBe(true); - expect(!!result).toBe(false); + function getErrorCallback (done) { + return (error) => { + expect(!!error).toBe(true) + // expect(!!result).toBe(false) - const validationErrors = books.simpleSchema().namedContext().validationErrors(); - expect(validationErrors.length).toBe(1); + const validationErrors = books.simpleSchema().namedContext().validationErrors() + expect(validationErrors.length).toBe(1) - done(); - }; + done() + } } it('valid', function (done) { - books.upsert({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'upsert', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { - title: "Ulysses", - author: "James Joyce", + title: 'Ulysses', + author: 'James Joyce', copies: 1 } - }, getCallback(done)); - }); + }]) + .then(getCallback(done)) + .catch(done) + }) it('upsert as update should update entity by _id - valid', function (done) { - const id = books.insert({title: 'new', author: 'author new', copies: 2}); - - books.upsert({ - _id: id - }, { - $set: { - title: "Ulysses", - author: "James Joyce", - copies: 1 - } - }, getCallback(done)); - }); + callMongoMethod(books, 'insert', [{ title: 'new', author: 'author new', copies: 2 }]) + .then((id) => { + return callMongoMethod(books, 'upsert', [{ + _id: id + }, { + $set: { + title: 'Ulysses', + author: 'James Joyce', + copies: 1 + } + }]) + }) + .then(getCallback(done)) + .catch(done) + }) it('upsert as update - valid', function (done) { - books.update({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'update', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { - title: "Ulysses", - author: "James Joyce", + title: 'Ulysses', + author: 'James Joyce', copies: 1 } }, { upsert: true - }, getUpdateCallback(done)); - }); + }]) + .then(getUpdateCallback(done)) + .catch(done) + }) it('upsert as update with $and', function (done) { - books.update({ + callMongoMethod(books, 'update', [{ $and: [ - { title: "Ulysses" }, - { author: "James Joyce" }, - ], + { title: 'Ulysses' }, + { author: 'James Joyce' } + ] }, { $set: { - title: "Ulysses", - author: "James Joyce", + title: 'Ulysses', + author: 'James Joyce', copies: 1 } }, { upsert: true - }, getUpdateCallback(done)); - }); + }]) + .then(getUpdateCallback(done)) + .catch(done) + }) it('upsert - invalid', function (done) { - books.upsert({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'upsert', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { copies: -1 } - }, getErrorCallback(done)); - }); + }]) + .then(() => done(new Error('should not get here'))) + .catch(getErrorCallback(done)) + }) it('upsert as update - invalid', function (done) { - books.update({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'update', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { copies: -1 } }, { upsert: true - }, getErrorCallback(done)); - }); + }]) + .then(() => done(new Error('should not get here'))) + .catch(getErrorCallback(done)) + }) it('upsert - valid with selector', function (done) { - books.upsert({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'upsert', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { copies: 1 } - }, getCallback(done)); - }); + }]) + .then(getCallback(done)) + .catch(done) + }) it('upsert as update - valid with selector', function (done) { - books.update({ - title: "Ulysses", - author: "James Joyce" + callMongoMethod(books, 'update', [{ + title: 'Ulysses', + author: 'James Joyce' }, { $set: { copies: 1 } }, { upsert: true - }, getUpdateCallback(done)); - }); - }); + }]) + .then(getUpdateCallback(done)) + .catch(done) + }) + }) } - it('validate false', function (done) { - let title; - if (Meteor.isClient) { - title = "Validate False Client"; - } else { - title = "Validate False Server"; - } - - books.insert({ - title: title, - author: "James Joyce" - }, { - validate: false, - validationContext: "validateFalse" - }, (error, result) => { - let insertedBook; - const validationErrors = books.simpleSchema().namedContext("validateFalse").validationErrors(); - - if (Meteor.isClient) { - // When validate: false on the client, we should still get a validation error and validationErrors back from the server - expect(!!error).toBe(true); - // There should be an `invalidKeys` property on the error, too - expect(error.invalidKeys.length).toBe(1); - expect(!!result).toBe(false); - expect(validationErrors.length).toBe(1); - - insertedBook = books.findOne({ title: title }); - expect(!!insertedBook).toBe(false); - } else { - // When validate: false on the server, validation should be skipped - expect(!!error).toBe(false); - expect(!!result).toBe(true); - expect(validationErrors.length).toBe(0); - - insertedBook = books.findOne({ title: title }); - expect(!!insertedBook).toBe(true); - } + if (Meteor.isServer) { + it('validate false', function (done) { + const title = 'Validate False Server' + let insertedBook, error, newId - // do a good one to set up update test - books.insert({ - title: title + " 2", - author: "James Joyce", - copies: 1 + callMongoMethod(books, 'insert', [{ + title, + author: 'James Joyce' }, { validate: false, - validationContext: "validateFalse2" - }, (error, newId) => { - const validationErrors = books.simpleSchema().namedContext("validateFalse2").validationErrors(); + validationContext: 'validateFalse' + }]) + .then(async (result) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse').validationErrors() + + // When validated: false on the server, validation should be skipped + expect(!!error).toBe(false) + expect(!!result).toBe(true) + expect(validationErrors.length).toBe(0) + + insertedBook = await callMongoMethod(books, 'findOne', [{ title }]) + expect(!!insertedBook).toBe(true) + + return callMongoMethod(books, 'insert', [{ + title: title + ' 2', + author: 'James Joyce', + copies: 1 + }, { + validate: false, + validationContext: 'validateFalse2' + }]) + }) + .then((_newId) => { + newId = _newId - expect(!!error).toBe(false); - expect(!!newId).toBe(true); - expect(validationErrors.length).toBe(0); + const validationErrors = books.simpleSchema().namedContext('validateFalse2').validationErrors() - const insertedBook = books.findOne({ title: title + " 2" }); - expect(!!insertedBook).toBe(true); + expect(!!newId).toBe(true) + expect(validationErrors.length).toBe(0) - books.update({ - _id: newId - }, { - $set: { - copies: "Yes Please" - } - }, { - validate: false, - validationContext: "validateFalse3" - }, (error, result) => { - let updatedBook; - const validationErrors = books.simpleSchema().namedContext("validateFalse3").validationErrors(); + return callMongoMethod(books, 'findOne', [{ title: title + ' 2' }]) + }) + .then((insertedBook) => { + expect(!!insertedBook).toBe(true) - if (Meteor.isClient) { - // When validate: false on the client, we should still get a validation error and invalidKeys from the server - expect(!!error).toBe(true); - // There should be an `invalidKeys` property on the error, too - expect(error.invalidKeys.length).toBe(1); - expect(!!result).toBe(false); - expect(validationErrors.length).toBe(1); - - updatedBook = books.findOne(newId); - expect(!!updatedBook).toBe(true); - // copies should still be 1 because our new value failed validation on the server - expect(updatedBook.copies).toBe(1); - } else { - // When validate: false on the server, validation should be skipped - expect(!!error).toBe(false); - expect(!!result).toBe(true); - expect(validationErrors.length).toBe(0); - - updatedBook = books.findOne(newId); - expect(!!updatedBook).toBe(true); - // copies should be changed despite being invalid because we skipped validation on the server - expect(updatedBook.copies).toBe('Yes Please'); - } - - // now try a good one - books.update({ + return callMongoMethod(books, 'update', [{ _id: newId }, { $set: { - copies: 3 + copies: 'Yes Please' } }, { validate: false, - validationContext: "validateFalse4" - }, (error, result) => { - const validationErrors = books.simpleSchema().namedContext("validateFalse4").validationErrors(); - expect(!!error).toBe(false); - expect(result).toBe(1); - expect(validationErrors.length).toBe(0); - - const updatedBook = books.findOne(newId); - expect(!!updatedBook).toBe(true); - // copies should be changed because we used a valid value - expect(updatedBook.copies).toBe(3); - done(); - }); - }); - }); - }); - }); + validationContext: 'validateFalse3' + }]) + }) + .then((result) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse3').validationErrors() + + // When validated: false on the server, validation should be skipped + expect(!!result).toBe(true) + expect(validationErrors.length).toBe(0) + return callMongoMethod(books, 'findOne', [newId]) + }) + .then((updatedBook) => { + expect(!!updatedBook).toBe(true) + // copies should be changed despite being invalid because we skipped validation on the server + expect(updatedBook.copies).toBe('Yes Please') + + return callMongoMethod(books, 'update', [{ + _id: newId + }, { + $set: { + copies: 3 + } + }, + { + validate: false, + validationContext: 'validateFalse4' + }]) + }) + .then((result) => { + const validationErrors = books.simpleSchema().namedContext('validateFalse4').validationErrors() + expect(result).toBe(1) + expect(validationErrors.length).toBe(0) + + return callMongoMethod(books, 'findOne', [newId]) + }) + .then((updatedBook) => { + expect(!!updatedBook).toBe(true) + // copies should be changed because we used a valid value + expect(updatedBook.copies).toBe(3) + done() + }) + .catch(done) + }) + } if (Meteor.isServer) { - it('bypassCollection2', function (done) { - let id; + it('bypassCollection2 5', async function () { + const id = await callMongoMethod(books, 'insert', [{}, { bypassCollection2: true }]) - try { - id = books.insert({}, {bypassCollection2: true}) - } catch (error) { - done(error); - } + await callMongoMethod(books, 'update', [id, { $set: { copies: 2 } }, { bypassCollection2: true }]) + }) + it('everything filtered out', async function () { try { - books.update(id, {$set: {copies: 2}}, {bypassCollection2: true}) - done(); - } catch (error) { - done(error); - } - }); - - it('everything filtered out', function () { - expect(function () { - upsertTest.update({_id: '123'}, { + await callMongoMethod(upsertTest, 'update', [{ _id: '123' }, { $set: { boo: 1 } - }); - }).toThrow('After filtering out keys not in the schema, your modifier is now empty'); - }); + }]) + } catch (e) { + expect(e.message).toBe('After filtering out keys not in the schema, your modifier is now empty') + } + }) - it('upsert works with schema that allows _id', function () { - upsertTest.remove({}); + it('upsert works with schema that allows _id', async function () { + await callMongoMethod(upsertTest, 'remove', [{}]) - const upsertTestId = upsertTest.insert({foo: 1}); + const upsertTestId = await callMongoMethod(upsertTest, 'insert', [{ foo: 1 }]) - upsertTest.update({_id: upsertTestId}, { + await callMongoMethod(upsertTest, 'update', [{ _id: upsertTestId }, { $set: { foo: 2 } }, { upsert: true - }); - const doc = upsertTest.findOne(upsertTestId); - expect(doc.foo).toBe(2); - }); + }]) + + const doc = await callMongoMethod(upsertTest, 'findOne', [upsertTestId]) + expect(doc.foo).toBe(2) + }) } } diff --git a/tests/clean.tests.js b/tests/clean.tests.js index de98665..1f9a741 100644 --- a/tests/clean.tests.js +++ b/tests/clean.tests.js @@ -1,109 +1,131 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { Meteor } from 'meteor/meteor' +import { callMongoMethod } from './helper' -let collection; +/* global describe it */ + +let collection if (Meteor.isClient) { - collection = new Mongo.Collection('cleanTests', { connection: null }); + collection = new Mongo.Collection('cleanTests', { connection: null }) } else { - collection = new Mongo.Collection('cleanTests'); + collection = new Mongo.Collection('cleanTests') } describe('clean options', function () { describe('filter', function () { it('keeps default schema clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - filter: false, - }, - }); + filter: false + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 'name', bad: 'prop' }, (error) => { - expect(error instanceof Error).toBe(true); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 'name', bad: 'prop' }]) + .then(() => { + done(new Error('Should not have inserted')) + }) + .catch(() => { + done() + }) + }) it('keeps operation clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - filter: true, - }, - }); + filter: true + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 'name', bad: 'prop' }, { filter: false }, (error) => { - expect(error instanceof Error).toBe(true); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 'name', bad: 'prop' }, { filter: false }]) + .then(() => { + done(new Error('Should not have inserted')) + }) + .catch(() => { + done() + }) + }) it('has clean option on by default', function (done) { - const schema = new SimpleSchema({ name: String }); + const schema = new SimpleSchema({ name: String }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 'name', bad: 'prop' }, (error) => { - expect(error).toBe(null); - done(); - }); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 'name', bad: 'prop' }]) + .then(() => { + done() + }) + .catch((err) => { + done(err) + }) + }) + }) describe('autoConvert', function () { it('keeps default schema clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - autoConvert: false, - }, - }); + autoConvert: false + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 1 }, (error) => { - expect(error instanceof Error).toBe(true); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 1 }]) + .then(() => { + done(new Error('Should not have inserted')) + }) + .catch(() => { + done() + }) + }) it('keeps operation clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - autoConvert: true, - }, - }); + autoConvert: true + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 1 }, { autoConvert: false }, (error) => { - expect(error instanceof Error).toBe(true); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 1 }, { autoConvert: false }]) + .then(() => { + done(new Error('Should not have inserted')) + }) + .catch(() => { + done() + }) + }) it('has clean option on by default', function (done) { - const schema = new SimpleSchema({ name: String }); + const schema = new SimpleSchema({ name: String }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: 1 }, (error) => { - expect(error).toBe(null); - done(); - }); - }); - }); + callMongoMethod(collection, 'insert', [{ name: 1 }]) + .then(() => { + done() + }) + .catch((err) => { + done(err) + }) + }) + }) describe('removeEmptyStrings', function () { it('keeps default schema clean options', function (done) { @@ -112,95 +134,116 @@ describe('clean options', function () { other: Number }, { clean: { - removeEmptyStrings: false, - }, - }); + removeEmptyStrings: false + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: '', other: 1 }, (error) => { - expect(error).toBe(null); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: '', other: 1 }]) + .then(() => { + done() + }) + .catch((err) => { + done(err) + }) + }) it('keeps operation clean options', function (done) { const schema = new SimpleSchema({ name: String, - other: Number, + other: Number }, { clean: { - removeEmptyStrings: true, - }, - }); + removeEmptyStrings: true + } + }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: '', other: 1 }, { removeEmptyStrings: false }, (error) => { - expect(error).toBe(null); - done(); - }); - }); + callMongoMethod(collection, 'insert', [{ name: '', other: 1 }, { removeEmptyStrings: false }]) + .then(() => { + done() + }) + .catch((err) => { + done(err) + }) + }) it('has clean option on by default', function (done) { - const schema = new SimpleSchema({ name: String, other: Number }); + const schema = new SimpleSchema({ name: String, other: Number }) - collection.attachSchema(schema, { replace: true }); + collection.attachSchema(schema, { replace: true }) - collection.insert({ name: '', other: 1 }, (error) => { - expect(error instanceof Error).toBe(true); - done(); - }); - }); - }); + callMongoMethod(collection, 'insert', [{ name: '', other: 1 }]) + .then(() => { + done(new Error('Should not have inserted')) + }) + .catch(() => { + done() + }) + }) + }) describe('trimStrings', function () { it('keeps default schema clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - trimStrings: false, - }, - }); - - collection.attachSchema(schema, { replace: true }); - - collection.insert({ name: ' foo ' }, (error, _id) => { - expect(error).toBe(null); - expect(collection.findOne(_id)).toEqual({ _id, name: ' foo ' }); - done(); - }); - }); + trimStrings: false + } + }) + + collection.attachSchema(schema, { replace: true }) + + callMongoMethod(collection, 'insert', [{ name: ' foo ' }]) + .then(async (_id) => { + const data = await callMongoMethod(collection, 'findOne', [_id]) + expect(data).toEqual({ _id, name: ' foo ' }) + done() + }) + .catch((err) => { + done(err) + }) + }) it('keeps operation clean options', function (done) { const schema = new SimpleSchema({ - name: String, + name: String }, { clean: { - trimStrings: true, - }, - }); - - collection.attachSchema(schema, { replace: true }); - - collection.insert({ name: ' foo ' }, { trimStrings: false }, (error, _id) => { - expect(error).toBe(null); - expect(collection.findOne(_id)).toEqual({ _id, name: ' foo ' }); - done(); - }); - }); + trimStrings: true + } + }) + + collection.attachSchema(schema, { replace: true }) + + callMongoMethod(collection, 'insert', [{ name: ' foo ' }, { trimStrings: false }]) + .then(async (_id) => { + const data = await callMongoMethod(collection, 'findOne', [_id]) + expect(data).toEqual({ _id, name: ' foo ' }) + done() + }) + .catch((err) => { + done(err) + }) + }) it('has clean option on by default', function (done) { - const schema = new SimpleSchema({ name: String }); - - collection.attachSchema(schema, { replace: true }); - - collection.insert({ name: ' foo ' }, (error, _id) => { - expect(error).toBe(null); - expect(collection.findOne(_id)).toEqual({ _id, name: 'foo' }); - done(); - }); - }); - }); -}); + const schema = new SimpleSchema({ name: String }) + + collection.attachSchema(schema, { replace: true }) + + callMongoMethod(collection, 'insert', [{ name: ' foo ' }]) + .then(async (_id) => { + const data = await callMongoMethod(collection, 'findOne', [_id]) + expect(data).toEqual({ _id, name: 'foo' }) + done() + }) + .catch((err) => { + done(err) + }) + }) + }) +}) diff --git a/tests/collection2.tests.js b/tests/collection2.tests.js index d1c7e4d..9071fce 100644 --- a/tests/collection2.tests.js +++ b/tests/collection2.tests.js @@ -1,404 +1,370 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; -import { _ } from 'meteor/underscore'; - -import addMultiTests from './multi.tests.js'; -import addBooksTests from './books.tests.js'; -import addContextTests from './context.tests.js'; -import addDefaultValuesTests from './default.tests.js'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import addMultiTests from './multi.tests.js' +import addBooksTests from './books.tests.js' +import addContextTests from './context.tests.js' +import addDefaultValuesTests from './default.tests.js' +import { Meteor } from 'meteor/meteor' +import { callMongoMethod } from './helper' + +/* global describe, it */ describe('collection2', function () { it('attach and get simpleSchema for normal collection', function () { - var mc = new Mongo.Collection('mc'); + const mc = new Mongo.Collection('mc', Meteor.isClient ? { connection: null } : undefined) mc.attachSchema( new SimpleSchema({ - foo: { type: String }, + foo: { type: String } }) - ); + ) - expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true); - }); + expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true) + }) it('attach and get simpleSchema for local collection', function () { - var mc = new Mongo.Collection(null); + const mc = new Mongo.Collection(null) mc.attachSchema( new SimpleSchema({ - foo: { type: String }, + foo: { type: String } }) - ); + ) - expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true); - }); + expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true) + }) - it('handles prototype-less objects', function (done) { - const prototypelessTest = new Mongo.Collection('prototypelessTest'); + it('handles prototype-less objects', async function () { + const prototypelessTest = new Mongo.Collection('prototypelessTest', Meteor.isClient ? { connection: null } : undefined) prototypelessTest.attachSchema( new SimpleSchema({ foo: { - type: String, - }, + type: String + } }) - ); + ) - const prototypelessObject = Object.create(null); - prototypelessObject.foo = 'bar'; + const prototypelessObject = Object.create(null) + prototypelessObject.foo = 'bar' - prototypelessTest.insert(prototypelessObject, (error, newId) => { - expect(!!error).toBe(false); - done(); - }); - }); + await callMongoMethod(prototypelessTest, 'insert', [prototypelessObject]) + }) if (Meteor.isServer) { // https://github.com/aldeed/meteor-collection2/issues/243 - it('upsert runs autoValue only once', function (done) { - const upsertAutoValueTest = new Mongo.Collection('upsertAutoValueTest'); - let times = 0; + it('upsert runs autoValue only once', async function () { + const upsertAutoValueTest = new Mongo.Collection('upsertAutoValueTest', Meteor.isClient ? { connection: null } : undefined) + let times = 0 upsertAutoValueTest.attachSchema( new SimpleSchema({ foo: { - type: String, + type: String }, av: { type: String, - autoValue() { - times++; - return 'test'; - }, - }, + autoValue () { + times++ + return 'test' + } + } }) - ); + ) - upsertAutoValueTest.remove({}); + await callMongoMethod(upsertAutoValueTest, 'remove', [{}]) - upsertAutoValueTest.upsert( - { - foo: 'bar', - }, - { - $set: { - av: 'abc', - }, - }, - (error, result) => { - expect(times).toBe(1); - done(); + await callMongoMethod(upsertAutoValueTest, 'upsert', [{ + foo: 'bar' + }, + { + $set: { + av: 'abc' } - ); - }); + }]) + expect(times).toBe(1) + }) // https://forums.meteor.com/t/simpl-schema-update-error-while-using-lte-operator-when-calling-update-by-the-field-of-type-date/50414/3 - it('upsert can handle query operators in the selector', function () { + it('upsert can handle query operators in the selector', async function () { const upsertQueryOperatorsTest = new Mongo.Collection( - 'upsertQueryOperatorsTest' - ); + 'upsertQueryOperatorsTest', + Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorsTest.attachSchema( new SimpleSchema({ foo: { type: Date, - optional: true, + optional: true }, bar: Number, - baz: Number, + baz: Number }) - ); + ) - upsertQueryOperatorsTest.remove({}); - const oneDayInMs = 1000 * 60 * 60 * 24; - const yesterday = new Date(Date.now() - oneDayInMs); - const tomorrow = new Date(Date.now() + oneDayInMs); + await callMongoMethod(upsertQueryOperatorsTest, 'remove', [{}]) + const oneDayInMs = 1000 * 60 * 60 * 24 + const yesterday = new Date(Date.now() - oneDayInMs) + const tomorrow = new Date(Date.now() + oneDayInMs) - const { numberAffected, insertedId } = upsertQueryOperatorsTest.upsert( + const { numberAffected, insertedId } = await callMongoMethod(upsertQueryOperatorsTest, 'upsert', [ { - foo: { $gte: yesterday, $lte: tomorrow }, + foo: { $gte: yesterday, $lte: tomorrow } }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - } - ); + baz: 4 + } + }]) + + expect(numberAffected).toBe(1) - expect(numberAffected).toBe(1); - const doc = upsertQueryOperatorsTest.findOne(); - expect(insertedId).toBe(doc._id); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); - }); + const doc = await callMongoMethod(upsertQueryOperatorsTest, 'findOne', []) + expect(insertedId).toBe(doc._id) + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + }) - it('upsert with schema can handle query operator which contains undefined or null', function (done) { + it('upsert with schema can handle query operator which contains undefined or null', async function () { const upsertQueryOperatorUndefinedTest = new Mongo.Collection( - 'upsertQueryOperatorUndefinedTest' - ); + 'upsertQueryOperatorUndefinedTest', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorUndefinedTest.attachSchema( new SimpleSchema({ foo: { type: String, - optional: true, + optional: true }, bar: Number, - baz: Number, + baz: Number }) - ); + ) // Let's try for undefined. - upsertQueryOperatorUndefinedTest.remove({}); + await callMongoMethod(upsertQueryOperatorUndefinedTest, 'remove', [{}]) - upsertQueryOperatorUndefinedTest.upsert( - { - foo: undefined, + const result = await callMongoMethod(upsertQueryOperatorUndefinedTest, 'upsert', [{ + foo: undefined + }, + { + $set: { + bar: 2 }, - { - $set: { - bar: 2, - }, - $inc: { - baz: 4, - }, + $inc: { + baz: 4 + } + }]) + + expect(result.numberAffected).toBe(1) + + const doc = await callMongoMethod(upsertQueryOperatorUndefinedTest, 'findOne', []) + expect(result.insertedId).toBe(doc._id) + expect(doc.foo).toBe(undefined) + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + + // Let's try for null. + + await callMongoMethod(upsertQueryOperatorUndefinedTest, 'remove', [{}]) + + const result2 = await callMongoMethod(upsertQueryOperatorUndefinedTest, 'upsert', [{ + foo: null + }, + { + $set: { + bar: 2 }, - (error, result) => { - expect(error).toBe(null); - - expect(result.numberAffected).toBe(1); - const doc = upsertQueryOperatorUndefinedTest.findOne(); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo).toBe(undefined); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); - - // Let's try for null. - upsertQueryOperatorUndefinedTest.remove({}); - - upsertQueryOperatorUndefinedTest.upsert( - { - foo: null, - }, - { - $set: { - bar: 2, - }, - $inc: { - baz: 4, - }, - }, - (error2, result2) => { - expect(error2).toBe(null); - - expect(result2.numberAffected).toBe(1); - const doc = upsertQueryOperatorUndefinedTest.findOne(); - expect(result2.insertedId).toBe(doc._id); - expect(doc.foo).toBe(null); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); - - done(); - } - ); + $inc: { + baz: 4 } - ); - }); + }]) + + expect(result2.numberAffected).toBe(1) - it('upsert with schema can handle query operator "eq" correctly in the selector when property is left out in $set or $setOnInsert', function (done) { + const doc2 = await callMongoMethod(upsertQueryOperatorUndefinedTest, 'findOne', []) + expect(result2.insertedId).toBe(doc2._id) + expect(doc2.foo).toBe(null) + expect(doc2.bar).toBe(2) + expect(doc2.baz).toBe(4) + }) + + it('upsert with schema can handle query operator "eq" correctly in the selector when property is left out in $set or $setOnInsert', async function () { const upsertQueryOperatorEqTest = new Mongo.Collection( - 'upsertQueryOperatorEqTest' - ); + 'upsertQueryOperatorEqTest', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorEqTest.attachSchema( new SimpleSchema({ foo: String, bar: Number, - baz: Number, + baz: Number }) - ); + ) - upsertQueryOperatorEqTest.remove({}); + await callMongoMethod(upsertQueryOperatorEqTest, 'remove', [{}]) - upsertQueryOperatorEqTest.upsert( + const result = await callMongoMethod(upsertQueryOperatorEqTest, 'upsert', [ { - foo: { $eq: 'test' }, + foo: { $eq: 'test' } }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - }, - (error, result) => { - expect(error).toBe(null); + baz: 4 + } + }]) - expect(result.numberAffected).toBe(1); - const doc = upsertQueryOperatorEqTest.findOne(); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo).toBe('test'); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); + expect(result.numberAffected).toBe(1) - done(); - } - ); - }); + const doc = await callMongoMethod(upsertQueryOperatorEqTest, 'findOne', []) + expect(result.insertedId).toBe(doc._id) + expect(doc.foo).toBe('test') + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + }) - it('upsert with schema can handle query operator "in" with one element correctly in the selector when property is left out in $set or $setOnInsert', function (done) { + it('upsert with schema can handle query operator "in" with one element correctly in the selector when property is left out in $set or $setOnInsert', async function () { const upsertQueryOperatorInSingleTest = new Mongo.Collection( - 'upsertQueryOperatorInSingleTest' - ); + 'upsertQueryOperatorInSingleTest', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorInSingleTest.attachSchema( new SimpleSchema({ foo: String, bar: Number, - baz: Number, + baz: Number }) - ); + ) - upsertQueryOperatorInSingleTest.remove({}); + await callMongoMethod(upsertQueryOperatorInSingleTest, 'remove', [{}]) - upsertQueryOperatorInSingleTest.upsert( + const result = await callMongoMethod(upsertQueryOperatorInSingleTest, 'upsert', [ { - foo: { $in: ['test'] }, + foo: { $in: ['test'] } }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - }, - (error, result) => { - expect(error).toBe(null); + baz: 4 + } + }]) - expect(result.numberAffected).toBe(1); - const doc = upsertQueryOperatorInSingleTest.findOne(); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo).toBe('test'); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); + expect(result.numberAffected).toBe(1) - done(); - } - ); - }); + const doc = await callMongoMethod(upsertQueryOperatorInSingleTest, 'findOne', []) + expect(result.insertedId).toBe(doc._id) + expect(doc.foo).toBe('test') + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + }) - it('upsert with schema can handle query operator "in" with multiple elements correctly in the selector when property is left out in $set or $setOnInsert', function (done) { + it('upsert with schema can handle query operator "in" with multiple elements correctly in the selector when property is left out in $set or $setOnInsert', async function () { const upsertQueryOperatorInMultiTest = new Mongo.Collection( - 'upsertQueryOperatorInMultiTest' - ); + 'upsertQueryOperatorInMultiTest', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorInMultiTest.attachSchema( new SimpleSchema({ foo: { type: String, - optional: true, + optional: true }, bar: Number, - baz: Number, + baz: Number }) - ); + ) - upsertQueryOperatorInMultiTest.remove({}); + await callMongoMethod(upsertQueryOperatorInMultiTest, 'remove', [{}]) - upsertQueryOperatorInMultiTest.upsert( + const result = await callMongoMethod(upsertQueryOperatorInMultiTest, 'upsert', [ { - foo: { $in: ['test', 'test2'] }, + foo: { $in: ['test', 'test2'] } }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - }, - (error, result) => { - expect(error).toBe(null); + baz: 4 + } + }]) - expect(result.numberAffected).toBe(1); - const doc = upsertQueryOperatorInMultiTest.findOne(); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo).toBe(undefined); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); + expect(result.numberAffected).toBe(1) - done(); - } - ); - }); + const doc = await callMongoMethod(upsertQueryOperatorInMultiTest, 'findOne', []) + expect(result.insertedId).toBe(doc._id) + expect(doc.foo).toBe(undefined) + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + }) // https://github.com/Meteor-Community-Packages/meteor-collection2/issues/408 - it('upsert with schema can handle nested objects correctly', function (done) { + it('upsert with schema can handle nested objects correctly', async function () { const upsertQueryOperatorNestedObject = new Mongo.Collection( - 'upsertQueryOperatorNestedObject' - ); + 'upsertQueryOperatorNestedObject', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorNestedObject.attachSchema( new SimpleSchema({ foo: { type: new SimpleSchema({ bar: { - type: String, + type: String }, baz: { - type: String, - }, - }), + type: String + } + }) }, test: { - type: Date, - }, + type: Date + } }) - ); + ) - upsertQueryOperatorNestedObject.remove({}); + await callMongoMethod(upsertQueryOperatorNestedObject, 'remove', [{}]) - const testDateValue = new Date(); - upsertQueryOperatorNestedObject.upsert( - { - test: '1', - }, - { - $set: { - foo: { - bar: '1', - baz: '2', - }, - test: testDateValue, + const testDateValue = new Date() + + const result = await callMongoMethod(upsertQueryOperatorNestedObject, 'upsert', [{ + test: '1' + }, + { + $set: { + foo: { + bar: '1', + baz: '2' }, - }, - (error, result) => { - expect(error).toBe(null); - expect(result.numberAffected).toBe(1); + test: testDateValue + } + }]) - const doc = upsertQueryOperatorNestedObject.findOne({ - _id: result.insertedId, - }); + expect(result.numberAffected).toBe(1) - expect(result.insertedId).toBe(doc._id); - expect(doc.foo.bar).toBe('1'); - expect(doc.foo.baz).toBe('2'); - expect(doc.test).toEqual(testDateValue); + const doc = await callMongoMethod(upsertQueryOperatorNestedObject, 'findOne', [{ + _id: result.insertedId + }]) - done(); - } - ); - }); + expect(result.insertedId).toBe(doc._id) + expect(doc.foo.bar).toBe('1') + expect(doc.foo.baz).toBe('2') + expect(doc.test).toEqual(testDateValue) + }) - it('upsert with schema can handle query operator "$and" including inner nested selectors correctly when properties is left out in $set or $setOnInsert', function (done) { + it('upsert with schema can handle query operator "$and" including inner nested selectors correctly when properties is left out in $set or $setOnInsert', async function () { const upsertQueryOperatorAndTest = new Mongo.Collection( - 'upsertQueryOperatorAndTest' - ); + 'upsertQueryOperatorAndTest', Meteor.isClient ? { connection: null } : undefined + ) upsertQueryOperatorAndTest.attachSchema( new SimpleSchema({ @@ -406,289 +372,247 @@ describe('collection2', function () { test1: String, test2: String, bar: Number, - baz: Number, + baz: Number }) - ); + ) - upsertQueryOperatorAndTest.remove({}); + await callMongoMethod(upsertQueryOperatorAndTest, 'remove', [{}]) - upsertQueryOperatorAndTest.upsert( + const result = await callMongoMethod(upsertQueryOperatorAndTest, 'upsert', [ { foo: 'test', - $and: [{ test1: 'abc' }, { $and: [{ test2: { $in: ['abc'] } }] }], + $and: [{ test1: 'abc' }, { $and: [{ test2: { $in: ['abc'] } }] }] }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - }, - (error, result) => { - expect(error).toBe(null); - - expect(result.numberAffected).toBe(1); - const doc = upsertQueryOperatorAndTest.findOne(); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo).toBe('test'); - expect(doc.test1).toBe('abc'); - expect(doc.test2).toBe('abc'); - expect(doc.bar).toBe(2); - expect(doc.baz).toBe(4); - - done(); - } - ); - }); + baz: 4 + } + }]) + + expect(result.numberAffected).toBe(1) + + const doc = await callMongoMethod(upsertQueryOperatorAndTest, 'findOne', []) + expect(result.insertedId).toBe(doc._id) + expect(doc.foo).toBe('test') + expect(doc.test1).toBe('abc') + expect(doc.test2).toBe('abc') + expect(doc.bar).toBe(2) + expect(doc.baz).toBe(4) + }) } - it('no errors when using a schemaless collection', function (done) { + it('no errors when using a schemaless collection', async function () { const noSchemaCollection = new Mongo.Collection('noSchema', { - transform(doc) { - doc.userFoo = 'userBar'; - return doc; + transform (doc) { + doc.userFoo = 'userBar' + return doc }, - }); - - noSchemaCollection.insert( - { - a: 1, - b: 2, - }, - (error, newId) => { - expect(!!error).toBe(false); - expect(!!newId).toBe(true); - - const doc = noSchemaCollection.findOne(newId); - expect(doc instanceof Object).toBe(true); - expect(doc.userFoo).toBe('userBar'); - - noSchemaCollection.update( - { - _id: newId, - }, - { - $set: { - a: 3, - b: 4, - }, - }, - (error) => { - expect(!!error).toBe(false); - done(); - } - ); + connection: Meteor.isClient ? null : undefined + }) + + const newId = await callMongoMethod(noSchemaCollection, 'insert', [{ + a: 1, + b: 2 + }]) + + expect(!!newId).toBe(true) + + const doc = await callMongoMethod(noSchemaCollection, 'findOne', [newId]) + expect(doc instanceof Object).toBe(true) + expect(doc.userFoo).toBe('userBar') + + await callMongoMethod(noSchemaCollection, 'update', [{ + _id: newId + }, + { + $set: { + a: 3, + b: 4 } - ); - }); + }]) + }) - it('empty strings are removed but we can override', function (done) { + it('empty strings are removed but we can override', async function () { const RESSchema = new SimpleSchema({ foo: { type: String }, - bar: { type: String, optional: true }, - }); + bar: { type: String, optional: true } + }) - const RES = new Mongo.Collection('RES'); - RES.attachSchema(RESSchema); + const RES = new Mongo.Collection('RES', Meteor.isClient ? { connection: null } : undefined) + RES.attachSchema(RESSchema) // Remove empty strings (default) - RES.insert( - { - foo: 'foo', - bar: '', - }, - (error, newId1) => { - expect(!!error).toBe(false); - expect(typeof newId1).toBe('string'); - - const doc = RES.findOne(newId1); - expect(doc instanceof Object).toBe(true); - expect(doc.bar).toBe(undefined); - - // Don't remove empty strings - RES.insert( - { - foo: 'foo', - bar: '', - }, - { - removeEmptyStrings: false, - }, - (error, newId2) => { - expect(!!error).toBe(false); - expect(typeof newId2).toBe('string'); - - const doc = RES.findOne(newId2); - expect(doc instanceof Object).toBe(true); - expect(doc.bar).toBe(''); - - // Don't remove empty strings for an update either - RES.update( - { - _id: newId1, - }, - { - $set: { - bar: '', - }, - }, - { - removeEmptyStrings: false, - }, - (error, result) => { - expect(!!error).toBe(false); - expect(result).toBe(1); - - const doc = RES.findOne(newId1); - expect(doc instanceof Object).toBe(true); - expect(doc.bar).toBe(''); - done(); - } - ); - } - ); + const newId1 = await callMongoMethod(RES, 'insert', [{ + foo: 'foo', + bar: '' + }]) + expect(typeof newId1).toBe('string') + + const doc = await callMongoMethod(RES, 'findOne', [newId1]) + expect(doc instanceof Object).toBe(true) + expect(doc.bar).toBe(undefined) + + // Don't remove empty strings + const newId2 = await callMongoMethod(RES, 'insert', [{ + foo: 'foo', + bar: '' + }, + { + removeEmptyStrings: false + }]) + + expect(typeof newId2).toBe('string') + + const doc2 = await callMongoMethod(RES, 'findOne', [newId2]) + expect(doc2 instanceof Object).toBe(true) + expect(doc2.bar).toBe('') + + // Don't remove empty strings for an update either + const result = await callMongoMethod(RES, 'update', [{ + _id: newId1 + }, + { + $set: { + bar: '' } - ); - }); - - it('extending a schema after attaching it, collection2 validation respects the extension', (done) => { + }, + { + removeEmptyStrings: false + }]) + + expect(result).toBe(1) + const doc3 = await callMongoMethod(RES, 'findOne', [newId1]) + expect(doc3 instanceof Object).toBe(true) + expect(doc3.bar).toBe('') + }) + + it('extending a schema after attaching it, collection2 validation respects the extension', async function () { const schema = new SimpleSchema({ - foo: String, - }); + foo: String + }) - const collection = new Mongo.Collection('ExtendAfterAttach'); - collection.attachSchema(schema); + const collection = new Mongo.Collection('ExtendAfterAttach', Meteor.isClient ? { connection: null } : undefined) + collection.attachSchema(schema) - collection.insert( - { + try { + await callMongoMethod(collection, 'insert', [{ foo: 'foo', - bar: 'bar', + bar: 'bar' }, { - filter: false, - }, - (error) => { - expect(error.invalidKeys[0].name).toBe('bar'); - schema.extend({ - bar: String, - }); - - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error2) => { - expect(!!error2).toBe(false); + filter: false + }]) + } catch (error) { + expect(error.invalidKeys[0].name).toBe('bar') - done(); - } - ); - } - ); - }); + schema.extend({ + bar: String + }) - it('extending a schema with a selector after attaching it, collection2 validation respects the extension', (done) => { - const schema = new SimpleSchema({ - foo: String, - }); + await callMongoMethod(collection, 'insert', [ + { + foo: 'foo', + bar: 'bar' + }, + { + filter: false + }]) - const collection = new Mongo.Collection('ExtendAfterAttach2'); - collection.attachSchema(schema, { selector: { foo: 'foo' } }); + return + } - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error) => { - expect(error.invalidKeys[0].name).toBe('bar'); + throw new Error('should not get here') + }) - schema.extend({ - bar: String, - }); + it('extending a schema with a selector after attaching it, collection2 validation respects the extension', async () => { + const schema = new SimpleSchema({ + foo: String + }) - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error2) => { - expect(!!error2).toBe(false); + const collection = new Mongo.Collection('ExtendAfterAttach2', Meteor.isClient ? { connection: null } : undefined) + collection.attachSchema(schema, { selector: { foo: 'foo' } }) - done(); - } - ); - } - ); - }); + try { + await callMongoMethod(collection, 'insert', [ + { + foo: 'foo', + bar: 'bar' + }, + { + filter: false + }]) + } catch (error) { + expect(error.invalidKeys[0].name).toBe('bar') + + schema.extend({ + bar: String + }) + + await callMongoMethod(collection, 'insert', [ + { + foo: 'foo', + bar: 'bar' + }, + { + filter: false + }]) + } + }) - it('pick or omit schema fields when options are provided', function () { + it('pick or omit schema fields when options are provided', async function () { const collectionSchema = new SimpleSchema({ foo: { type: String }, - bar: { type: String, optional: true }, - }); + bar: { type: String, optional: true } + }) - const collection = new Mongo.Collection('pickOrOmit'); - collection.attachSchema(collectionSchema); + const collection = new Mongo.Collection('pickOrOmit', Meteor.isClient ? { connection: null } : undefined) + collection.attachSchema(collectionSchema) - // Test error from including both pick and omit - let errorThrown = false; + // Test error from including both pick and omit 2 + let errorThrown = false try { - collection.insert( + await callMongoMethod(collection, 'insert', [ { foo: 'foo', bar: '' }, { pick: ['foo'], omit: ['foo'] } - ); + ]) } catch (error) { expect(error.message).toBe( 'pick and omit options are mutually exclusive' - ); - errorThrown = true; + ) + errorThrown = true } - expect(errorThrown).toBe(true); + expect(errorThrown).toBe(true) // should have thrown error // Omit required field 'foo' - collection.insert({ bar: 'test' }, { omit: ['foo'] }, (error, newId2) => { - expect(!!error).toBe(false); - expect(typeof newId2).toBe('string'); - - const doc = collection.findOne(newId2); - expect(doc instanceof Object).toBe(true); - expect(doc.foo).toBe(undefined); - expect(doc.bar).toBe('test'); - - // Pick only 'foo' - collection.update( - { _id: newId2 }, - { $set: { foo: 'test', bar: 'changed' } }, - { pick: ['foo'] }, - (error, result) => { - expect(!!error).toBe(false); - expect(result).toBe(1); - - const doc = collection.findOne(newId2); - expect(doc instanceof Object).toBe(true); - expect(doc.foo).toBe('test'); - expect(doc.bar).toBe('test'); - } - ); - }); - }); - - addBooksTests(); - addContextTests(); - addDefaultValuesTests(); - addMultiTests(); -}); + const newId2 = await callMongoMethod(collection, 'insert', [{ bar: 'test' }, { omit: ['foo'] }]) + + expect(typeof newId2).toBe('string') + + const doc = await callMongoMethod(collection, 'findOne', [newId2]) + expect(doc instanceof Object).toBe(true) + expect(doc.foo).toBe(undefined) + expect(doc.bar).toBe('test') + + // Pick only 'foo' + const result = await callMongoMethod(collection, 'update', [{ _id: newId2 }, + { $set: { foo: 'test', bar: 'changed' } }, + { pick: ['foo'] }]) + + expect(result).toBe(1) + + const doc2 = await callMongoMethod(collection, 'findOne', [newId2]) + expect(doc2 instanceof Object).toBe(true) + expect(doc2.foo).toBe('test') + expect(doc2.bar).toBe('test') + }) + + addBooksTests() + addContextTests() + addDefaultValuesTests() + addMultiTests() +}) diff --git a/tests/context.tests.js b/tests/context.tests.js index 24d6e04..4afba34 100644 --- a/tests/context.tests.js +++ b/tests/context.tests.js @@ -1,6 +1,10 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { Meteor } from 'meteor/meteor' +import { callMongoMethod } from './helper' + +/* global it */ const contextCheckSchema = new SimpleSchema({ foo: { @@ -10,96 +14,85 @@ const contextCheckSchema = new SimpleSchema({ context: { type: Object, optional: true, - defaultValue: {}, + defaultValue: {} }, 'context.userId': { type: String, optional: true, - autoValue() { - return this.userId; + autoValue () { + return this.userId } }, 'context.isFromTrustedCode': { type: Boolean, optional: true, - autoValue() { - return this.isFromTrustedCode; + autoValue () { + return this.isFromTrustedCode } }, 'context.isInsert': { type: Boolean, optional: true, - autoValue() { - return this.isInsert; + autoValue () { + return this.isInsert } }, 'context.isUpdate': { type: Boolean, optional: true, - autoValue() { - return this.isUpdate; + autoValue () { + return this.isUpdate } }, 'context.docId': { type: String, optional: true, - autoValue() { - return this.docId; + autoValue () { + return this.docId } } -}); - -const contextCheck = new Mongo.Collection('contextCheck'); -contextCheck.attachSchema(contextCheckSchema); +}) -export default function addContextTests() { - it('AutoValue Context', function (done) { - let testId; +const contextCheck = new Mongo.Collection('contextCheck') +contextCheck.attachSchema(contextCheckSchema) - const callback1 = () => { - const ctx = contextCheck.findOne(testId); - expect(ctx.context.docId).toBe(testId); - done(); - }; +export default function addContextTests () { + it('AutoValue Context', async function () { + const testId = await callMongoMethod(contextCheck, 'insert', [{}]) - const callback2 = () => { - const ctx = contextCheck.findOne(testId); - expect(ctx.foo).toBe('bar'); - expect(ctx.context.isUpdate).toBe(true); - expect(ctx.context.isInsert).toBe(false); - expect(ctx.context.userId).toBe(null); - expect(ctx.context.docId).toBe(testId); - expect(ctx.context.isFromTrustedCode).toBe(!Meteor.isClient); + let ctx = await callMongoMethod(contextCheck, 'findOne', [testId]) + expect(ctx.context.isInsert).toBe(true) + expect(ctx.context.isUpdate).toBe(false) + expect(ctx.context.userId).toBe(null) + expect(ctx.context.docId).toBe(undefined) + expect(ctx.context.isFromTrustedCode).toBe(!Meteor.isClient) - // make sure docId works with `_id` direct, too - contextCheck.update(testId, { - $set: { - context: {}, - foo: "bar" - } - }, callback1); - }; + await callMongoMethod(contextCheck, 'update', [{ + _id: testId + }, { + $set: { + context: {}, + foo: 'bar' + } + }]) - const callback3 = (error, result) => { - testId = result; - expect(!!error).toBe(false); - const ctx = contextCheck.findOne(testId); - expect(ctx.context.isInsert).toBe(true); - expect(ctx.context.isUpdate).toBe(false); - expect(ctx.context.userId).toBe(null); - expect(ctx.context.docId).toBe(undefined); - expect(ctx.context.isFromTrustedCode).toBe(!Meteor.isClient); + ctx = await callMongoMethod(contextCheck, 'findOne', [testId]) + expect(ctx.foo).toBe('bar') + expect(ctx.context.isUpdate).toBe(true) + expect(ctx.context.isInsert).toBe(false) + expect(ctx.context.userId).toBe(null) + expect(ctx.context.docId).toBe(testId) + expect(ctx.context.isFromTrustedCode).toBe(!Meteor.isClient) - contextCheck.update({ - _id: testId - }, { - $set: { - context: {}, - foo: "bar" - } - }, callback2); - }; + // make sure docId works with `_id` direct, too + await callMongoMethod(contextCheck, 'update', [testId, { + $set: { + context: {}, + foo: 'bar' + } + }]) - contextCheck.insert({}, callback3); - }); + ctx = await callMongoMethod(contextCheck, 'findOne', [testId]) + expect(ctx.context.docId).toBe(testId) + }) } diff --git a/tests/default.tests.js b/tests/default.tests.js index 8d4e0ae..69d717b 100644 --- a/tests/default.tests.js +++ b/tests/default.tests.js @@ -1,43 +1,74 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { Meteor } from 'meteor/meteor' +import { callMongoMethod } from './helper' + +/* global it */ const defaultValuesSchema = new SimpleSchema({ bool1: { type: Boolean, defaultValue: false } -}); +}) -const defaultValues = new Mongo.Collection('dv'); -defaultValues.attachSchema(defaultValuesSchema); +const defaultValues = new Mongo.Collection('dv') +defaultValues.attachSchema(defaultValuesSchema) +global.defaultValues = defaultValues -export default function addDefaultValuesTests() { - it('defaultValues', function (done) { - let p; +export default function addDefaultValuesTests () { + if (Meteor.isServer) { + it('defaultValues', function (done) { + let p - // Base case - defaultValues.insert({}, (error, testId1) => { - p = defaultValues.findOne(testId1); - expect(p.bool1).toBe(false); + // Base case + callMongoMethod(defaultValues, 'insert', [{}]) + .then(async (testId1) => { + p = await callMongoMethod(defaultValues, 'findOne', [testId1]) + expect(p.bool1).toBe(false) - // Ensure that default values do not mess with inserts and updates of the field - defaultValues.insert({ - bool1: true - }, (err, testId2) => { - p = defaultValues.findOne(testId2); - expect(p.bool1).toBe(true); - - defaultValues.update(testId1, { - $set: { + // Ensure that default values do not mess with inserts and updates of the field + callMongoMethod(defaultValues, 'insert', [{ bool1: true - } - }, () => { - p = defaultValues.findOne(testId1); - expect(p.bool1).toBe(true); - done(); - }); - }); - }); - }); + }]).then(async (testId2) => { + p = await callMongoMethod(defaultValues, 'findOne', [testId2]) + expect(p.bool1).toBe(true) + + callMongoMethod(defaultValues, 'update', [testId1, { + $set: { + bool1: true + } + }]).then(async () => { + p = await callMongoMethod(defaultValues, 'findOne', [testId1]) + expect(p.bool1).toBe(true) + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + } else { + it('defaultValues', async function () { + // Base case + const testId1 = await callMongoMethod(defaultValues, 'insert', [{}]) + let p = await callMongoMethod(defaultValues, 'findOne', [testId1]) + expect(p.bool1).toBe(false) + + // Ensure that default values do not mess with inserts and updates of the field + const testId2 = await callMongoMethod(defaultValues, 'insert', [{ bool1: true }]) + p = await callMongoMethod(defaultValues, 'findOne', [testId2]) + expect(p.bool1).toBe(true) + + await callMongoMethod(defaultValues, 'update', [testId1, { + $set: { + bool1: true + } + }]) + p = await callMongoMethod(defaultValues, 'findOne', [testId1]) + expect(p.bool1).toBe(true) + }) + } }; diff --git a/tests/defaultCleanOptions.tests.js b/tests/defaultCleanOptions.tests.js index 8cbac2a..98a57c3 100644 --- a/tests/defaultCleanOptions.tests.js +++ b/tests/defaultCleanOptions.tests.js @@ -1,27 +1,29 @@ -import expect from 'expect'; -import Collection2 from 'meteor/aldeed:collection2'; +import expect from 'expect' +import Collection2 from 'meteor/aldeed:collection2' + +/* global it describe */ describe('cleanOptions', function () { - it('comes preloaded with default values', function() { - expect(Collection2.cleanOptions).toEqual({ - filter: true, - autoConvert: true, - removeEmptyStrings: true, - trimStrings: true, - removeNullsFromArrays: false, - }); - }); + it('comes preloaded with default values', function () { + expect(Collection2.cleanOptions).toEqual({ + filter: true, + autoConvert: true, + removeEmptyStrings: true, + trimStrings: true, + removeNullsFromArrays: false + }) + }) it('allows setting cleanOptions', function () { - const cleanOptions = { - filter: false, - autoConvert: false, - removeEmptyStrings: false, - trimStrings: false, - removeNullsFromArrays: false, - }; + const cleanOptions = { + filter: false, + autoConvert: false, + removeEmptyStrings: false, + trimStrings: false, + removeNullsFromArrays: false + } - Collection2.cleanOptions = cleanOptions; - expect(Collection2.cleanOptions).toEqual(cleanOptions); - }); - }); \ No newline at end of file + Collection2.cleanOptions = cleanOptions + expect(Collection2.cleanOptions).toEqual(cleanOptions) + }) +}) diff --git a/tests/helper.js b/tests/helper.js new file mode 100644 index 0000000..b71477e --- /dev/null +++ b/tests/helper.js @@ -0,0 +1,57 @@ +import { Meteor } from 'meteor/meteor' +import { Mongo } from 'meteor/mongo' + +let strategy = null +if (Mongo.Collection.prototype.insertAsync && Meteor.isFibersDisabled) { + strategy = 3 +} else if (Mongo.Collection.prototype.insertAsync) { + strategy = 2 +} else { + strategy = 1 +} + +function getMethodNameByMeteorVersion (methodName) { + if (strategy === 1) { + return methodName + } + + return `${methodName}Async` +} + +export function callMongoMethod (collection, method, args) { + const methodName = getMethodNameByMeteorVersion(method) + + return new Promise((resolve, reject) => { + if (strategy <= 2) { + if (Meteor.isClient && !['findOne', 'findOneAsync'].includes(methodName)) { + collection[methodName](...args, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result) + } + }) + } else { + try { + resolve(collection[methodName](...args)) + } catch (error) { + reject(error) + } + } + } else { + collection[methodName](...args) + .then(resolve) + .catch(reject) + } + }) +} + +export function callMeteorFetch (collection, selector) { + return new Promise((resolve, reject) => { + if (strategy === 1) { + resolve(collection.find(selector).fetch()) + } else { + resolve(collection.find(selector).fetchAsync()) + } + }) +} diff --git a/tests/multi.tests.js b/tests/multi.tests.js index 29e0032..b160845 100644 --- a/tests/multi.tests.js +++ b/tests/multi.tests.js @@ -1,6 +1,10 @@ -import expect from 'expect'; -import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import expect from 'expect' +import { Mongo } from 'meteor/mongo' +import SimpleSchema from 'simpl-schema' +import { Meteor } from 'meteor/meteor' +import { callMeteorFetch, callMongoMethod } from './helper' + +/* global describe, it, beforeEach */ const productSchema = new SimpleSchema({ _id: { @@ -9,18 +13,18 @@ const productSchema = new SimpleSchema({ }, title: { type: String, - defaultValue: "" + defaultValue: '' }, type: { - label: "Product Type", + label: 'Product Type', type: String, - defaultValue: "simple" + defaultValue: 'simple' }, description: { type: String, - defaultValue: "This is a simple product." + defaultValue: 'This is a simple product.' } -}); +}) const productVariantSchema = new SimpleSchema({ _id: { @@ -29,390 +33,427 @@ const productVariantSchema = new SimpleSchema({ }, title: { type: String, - defaultValue: "" + defaultValue: '' }, optionTitle: { - label: "Option", + label: 'Option', type: String, optional: true }, type: { - label: "Product Variant Type", + label: 'Product Variant Type', type: String, - defaultValue: "variant" + defaultValue: 'variant' }, price: { - label: "Price", + label: 'Price', type: Number, min: 0, optional: true, defaultValue: 5 }, createdAt: { - type: Date, + type: Date } -}); +}) -const extendedProductSchema = new SimpleSchema(productSchema); +const extendedProductSchema = new SimpleSchema(productSchema) extendedProductSchema.extend({ barcode: { type: String, - defaultValue: "ABC123" + defaultValue: 'ABC123' } -}); +}) /* Products */ // Need to define the client one on both client and server -let products = new Mongo.Collection('TestProductsClient'); -products.attachSchema(productSchema, { selector: { type: 'simple' } }); -products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }); +let products = new Mongo.Collection('TestProductsClient') +products.attachSchema(productSchema, { selector: { type: 'simple' } }) +products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }) if (Meteor.isServer) { - products = new Mongo.Collection('TestProductsServer'); - products.attachSchema(productSchema, { selector: { type: 'simple' } }); - products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }); + products = new Mongo.Collection('TestProductsServer') + products.attachSchema(productSchema, { selector: { type: 'simple' } }) + products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }) } /* Extended Products */ // Need to define the client one on both client and server -let extendedProducts = new Mongo.Collection('ExtendedProductsClient'); -extendedProducts.attachSchema(productSchema, {selector: {type: 'simple'}}); -extendedProducts.attachSchema(productVariantSchema, {selector: {type: 'variant'}}); -extendedProducts.attachSchema(extendedProductSchema, {selector: {type: 'simple'}}); +let extendedProducts = new Mongo.Collection('ExtendedProductsClient') +extendedProducts.attachSchema(productSchema, { selector: { type: 'simple' } }) +extendedProducts.attachSchema(productVariantSchema, { selector: { type: 'variant' } }) +extendedProducts.attachSchema(extendedProductSchema, { selector: { type: 'simple' } }) if (Meteor.isServer) { - extendedProducts = new Mongo.Collection('ExtendedProductsServer'); - extendedProducts.attachSchema(productSchema, {selector: {type: 'simple'}}); - extendedProducts.attachSchema(productVariantSchema, {selector: {type: 'variant'}}); - extendedProducts.attachSchema(extendedProductSchema, {selector: {type: 'simple'}}); + extendedProducts = new Mongo.Collection('ExtendedProductsServer') + extendedProducts.attachSchema(productSchema, { selector: { type: 'simple' } }) + extendedProducts.attachSchema(productVariantSchema, { selector: { type: 'variant' } }) + extendedProducts.attachSchema(extendedProductSchema, { selector: { type: 'simple' } }) } -export default function addMultiTests() { +export default function addMultiTests () { describe('multiple top-level schemas', function () { - beforeEach(function () { - products.find({}).forEach(doc => { - products.remove(doc._id); - }); - extendedProducts.find({}).forEach(doc => { - products.remove(doc._id); - }); - }); + beforeEach(async function () { + for (const doc of await callMeteorFetch(products, {})) { + await callMongoMethod(products, 'remove', [doc._id]) + } + + for (const doc of await callMeteorFetch(extendedProducts, {})) { + await callMongoMethod(products, 'remove', [doc._id]) + } + + /* + for await (const doc of products.find({})) { + await products.removeAsync(doc._id); + } + + for await (const doc of extendedProducts.find({})) { + await products.removeAsync(doc._id); + } + */ + }) it('works', function () { - const c = new Mongo.Collection('multiSchema'); + const c = new Mongo.Collection('multiSchema') // Attach two different schemas c.attachSchema(new SimpleSchema({ one: { type: String } - })); + })) c.attachSchema(new SimpleSchema({ two: { type: String } - })); + })) // Check the combined schema - let combinedSchema = c.simpleSchema(); - expect(combinedSchema._schemaKeys.includes('one')).toBe(true); - expect(combinedSchema._schemaKeys.includes('two')).toBe(true); - expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(String)); + let combinedSchema = c.simpleSchema() + expect(combinedSchema._schemaKeys.includes('one')).toBe(true) + expect(combinedSchema._schemaKeys.includes('two')).toBe(true) + expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(String)) // Attach a third schema and make sure that it extends/overwrites the others c.attachSchema(new SimpleSchema({ two: { type: SimpleSchema.Integer } - })); - combinedSchema = c.simpleSchema(); - expect(combinedSchema._schemaKeys.includes('one')).toBe(true); - expect(combinedSchema._schemaKeys.includes('two')).toBe(true); - expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(SimpleSchema.Integer)); + })) + combinedSchema = c.simpleSchema() + expect(combinedSchema._schemaKeys.includes('one')).toBe(true) + expect(combinedSchema._schemaKeys.includes('two')).toBe(true) + expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(SimpleSchema.Integer)) // Ensure that we've only attached two deny functions - expect(c._validators.insert.deny.length).toBe(2); - expect(c._validators.update.deny.length).toBe(2); - }); + expect(c._validators.insert.deny.length).toBe(2) + expect(c._validators.update.deny.length).toBe(2) + }) - it('inserts doc correctly with selector passed via doc', function (done) { - const productId = products.insert({ - title: 'Product one', - type: 'simple' // selector in doc - }, () => { - const product = products.findOne(productId); - expect(product.description).toBe('This is a simple product.'); - expect(product.price).toBe(undefined); + if (Meteor.isServer) { + it('inserts doc correctly with selector passed via doc', async function () { + const productId = await callMongoMethod(products, 'insert', [{ + title: 'Product one', + type: 'simple' // selector in doc + }]) + + const product = await callMongoMethod(products, 'findOne', [productId]) + expect(product.description).toBe('This is a simple product.') + expect(product.price).toBe(undefined) - const productId3 = products.insert({ + const productId3 = await callMongoMethod(products, 'insert', [{ title: 'Product three', createdAt: new Date(), type: 'variant' // other selector in doc - }, () => { - const product3 = products.findOne(productId3); - expect(product3.description).toBe(undefined); - expect(product3.price).toBe(5); - done(); - }); - }); - }); + }]) + const product3 = await callMongoMethod(products, 'findOne', [productId3]) + expect(product3.description).toBe(undefined) + expect(product3.price).toBe(5) + }) - if (Meteor.isServer) { // Passing selector in options works only on the server because // client options are not sent to the server and made availabe in // the deny functions, where we call .simpleSchema() // // Also synchronous only works on server - it('insert selects the correct schema', function () { - const productId = products.insert({ + it('insert selects the correct schema', async function () { + const productId = await callMongoMethod(products, 'insert', [{ title: 'Product one' - }, { selector: { type: 'simple' } }); + }, { selector: { type: 'simple' } }]) - const productVariantId = products.insert({ + const productVariantId = await callMongoMethod(products, 'insert', [{ title: 'Product variant one', createdAt: new Date() - }, { selector: { type: 'variant' } }); + }, { selector: { type: 'variant' } }]) - const product = products.findOne(productId); - const productVariant = products.findOne(productVariantId); + const product = await callMongoMethod(products, 'findOne', [productId]) + const productVariant = await callMongoMethod(products, 'findOne', [productVariantId]) // we should receive new docs with correct property set for each type of doc - expect(product.description).toBe('This is a simple product.'); - expect(product.price).toBe(undefined); - expect(productVariant.description).toBe(undefined); + expect(product.description).toBe('This is a simple product.') + expect(product.price).toBe(undefined) + expect(productVariant.description).toBe(undefined) expect(productVariant.price).toBe(5) - }); + }) - it('inserts doc correctly with selector passed via doc and via