diff --git a/ .prettierignore b/ .prettierignore new file mode 100644 index 0000000..1b07c39 --- /dev/null +++ b/ .prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage \ No newline at end of file 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/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e146c5e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "none", + "singleQuote": true, + "printWidth": 100 + } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b41847..9311f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [4.0.0-beta.5](#400-beta.3) - [3.5.0](#350) - [3.4.1](#341) - [3.4.0](#340) @@ -77,6 +78,17 @@ +## 4.0.0 + +- Make collection2 compatible with Meteor 3.0 thanks to the [awesome work](https://github.com/Meteor-Community-Packages/meteor-collection2/pull/443) by @klablink +- Remove clean options https://github.com/Meteor-Community-Packages/meteor-collection2/pull/436 +- Prettier is added to enforce a consistent style across the project +- Removed `babel-polyfill` +- Updated expect to `26.6.2` +- Use [`aldeed:simple-schema@1.13.1`](https://github.com/Meteor-Community-Packages/meteor-simple-schema/releases/tag/v1.13.1) instead of [NPM version](https://github.com/longshotlabs/simpl-schema). +- Add static & dynamic loading +- Fix error with quave:synced-cron + ## 3.5.0 Add the ability to override in-built schema clean options. diff --git a/README.md b/README.md index b948fe8..5cd8a80 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ This package requires the [simpl-schema](https://github.com/aldeed/simple-schema - [Installation](#installation) +- [Import using static imports](#import-using-static-imports) +- [Import using dynamic imports](#import-using-dynamic-imports) - [Why Use Collection2](#why-use-collection2) - [Attaching a Schema to a Collection](#attaching-a-schema-to-a-collection) - [Attaching Multiple Schemas to the Same Collection](#attaching-multiple-schemas-to-the-same-collection) @@ -59,11 +61,40 @@ This package requires the [simpl-schema](https://github.com/aldeed/simple-schema In your Meteor app directory, enter: +### 4.0 + +Starting 4.0 collection2 ships with built in [`aldeed:simple-schema@1.13.1`](https://github.com/Meteor-Community-Packages/meteor-simple-schema/releases/tag/v1.13.1). + +```bash +meteor add aldeed:collection2 +``` + +### 3.x + ```bash meteor add aldeed:collection2 -meteor npm install --save simpl-schema +meteor npm install --save simpl-schema@1.12.3 +``` + +## Import using static imports + +If you come from a previous version and want to "keep things as they were" then this is the option you should choose. + +```js +import 'meteor/aldeed:collection2/static'; +``` + +## Import using dynamic imports + +Dynamic imports helps to cut down on bundle size but it requires you to manually load the package for things to work. + +```js +import 'meteor/aldeed:collection2/dynamic'; + +Collection2.load(); ``` + ## Why Use Collection2 - While adding allow/deny rules ensures that only authorized users can edit a document from the client, adding a schema ensures that only acceptable properties and values can be set within that document from the client. Thus, client side inserts and updates can be allowed without compromising security or data integrity. @@ -361,26 +392,6 @@ instance for a Mongo.Collection instance. For example: MyCollection.simpleSchema().validate(doc); ``` -## Schema Clean Options - -You can set the simpl-schema clean options globally in collection2. They are merged with any options defined on the schema level. - -```js -import Collection2 from 'meteor/aldeed:collection2' - -// The values shown are the default options used internally. Overwrite them if needed. -Collection2.cleanOptions = { - filter: true, - autoConvert: true, - removeEmptyStrings: true, - trimStrings: true, - removeNullsFromArrays: true, -} - -// Or you can update individual options. -Collection2.cleanOptions.filter = false; -``` - ## Passing Options In Meteor, the `update` function accepts an options argument. Collection2 changes the `insert` function signature to also accept options in the same way, as an optional second argument. Whenever this documentation says to "use X option", it's referring to this options argument. For example: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c931c11 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "meteor-collection2", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b3a1168 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "meteor-collection2", + "version": "1.0.0", + "description": "[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) ![GitHub](https://img.shields.io/github/license/Meteor-Community-Packages/meteor-collection2) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Meteor-Community-Packages/meteor-collection2.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Meteor-Community-Packages/meteor-collection2/context:javascript) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/Meteor-Community-Packages/meteor-collection2?label=latest&sort=semver) [![](https://img.shields.io/badge/semver-2.0.0-success)](http://semver.org/spec/v2.0.0.html) ![Test suite](https://github.com/Meteor-Community-Packages/meteor-collection2/workflows/Test%20suite/badge.svg)", + "main": "index.js", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Meteor-Community-Packages/meteor-collection2.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Meteor-Community-Packages/meteor-collection2/issues" + }, + "homepage": "https://github.com/Meteor-Community-Packages/meteor-collection2#readme", + "devDependencies": { + "prettier": "3.1.1" + } +} diff --git a/package/collection2/.versions b/package/collection2/.versions index 3b51fae..d6a4e7b 100644 --- a/package/collection2/.versions +++ b/package/collection2/.versions @@ -1,50 +1,56 @@ -aldeed:collection2@3.5.0 -allow-deny@1.1.0 -babel-compiler@7.7.0 -babel-runtime@1.5.0 +aldeed:collection2@4.0.0-beta.7 +aldeed:simple-schema@1.13.1 +allow-deny@1.1.1 +babel-compiler@7.10.5 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -boilerplate-generator@1.7.1 -callback-hook@1.3.1 -check@1.3.1 -ddp@1.4.0 -ddp-client@2.5.0 +boilerplate-generator@1.7.2 +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.4.0 -diff-sequence@1.1.1 -dynamic-import@0.7.1 -ecmascript@0.15.3 -ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.1 -ecmascript-runtime-server@0.10.1 -ejson@1.1.1 -fetch@0.1.1 -geojson-utils@1.0.10 +ddp-server@2.7.0 +diff-sequence@1.1.2 +dynamic-import@0.7.3 +ecmascript@0.16.8 +ecmascript-runtime@0.8.1 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +ejson@1.1.3 +fetch@0.1.4 +geojson-utils@1.0.11 +http@1.0.10 id-map@1.1.1 inter-process-messaging@0.1.1 -logging@1.2.0 -meteor@1.9.3 -minimongo@1.7.0 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.12.0 -mongo-decimal@0.1.2 +local-test:aldeed:collection2@4.0.0-beta.7 +logging@1.3.3 +meteor@1.11.4 +meteortesting:browser-tests@1.6.0-beta300.0 +meteortesting:mocha@3.1.0-beta300.0 +meteortesting:mocha-core@8.3.1-beta300.0 +minimongo@1.9.3 +modern-browsers@0.1.10 +modules@0.20.0 +modules-runtime@0.13.1 +mongo@1.16.8 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -npm-mongo@3.9.1 +npm-mongo@4.17.2 ordered-dict@1.1.0 -promise@0.12.0 +promise@0.12.2 raix:eventemitter@1.0.0 -random@1.2.0 -react-fast-refresh@0.1.1 +random@1.2.1 +react-fast-refresh@0.2.8 reload@1.3.1 retry@1.1.0 routepolicy@1.1.1 -socket-stream-client@0.4.0 -tmeasday:check-npm-versions@1.0.2 -tracker@1.2.0 -typescript@4.3.5 -underscore@1.0.10 -webapp@1.11.1 -webapp-hashing@1.1.0 +socket-stream-client@0.5.2 +tracker@1.3.3 +typescript@4.9.5 +underscore@1.0.13 +url@1.3.2 +webapp@1.13.6 +webapp-hashing@1.1.1 diff --git a/package/collection2/collection2.js b/package/collection2/collection2.js index 97073a9..b8b3eac 100644 --- a/package/collection2/collection2.js +++ b/package/collection2/collection2.js @@ -1,737 +1,6 @@ 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'; - -checkNpmVersions({ 'simpl-schema': '>=0.0.0' }, 'aldeed:collection2'); - -const SimpleSchema = require('simpl-schema').default; // Exported only for listening to events -const Collection2 = new EventEmitter(); - -Collection2.cleanOptions = { - filter: true, - autoConvert: true, - removeEmptyStrings: true, - trimStrings: true, - removeNullsFromArrays: false, -}; - -/** - * Mongo.Collection.prototype.attachSchema - * @param {SimpleSchema|Object} ss - SimpleSchema instance or a schema definition object - * from which to create a new SimpleSchema instance - * @param {Object} [options] - * @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 - * @return {undefined} - * - * Use this method to attach a schema to a collection created by another package, - * such as Meteor.users. It is most likely unsafe to call this method more than - * 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 || {}; - - // Allow passing just the schema object - if (!SimpleSchema.isSimpleSchema(ss)) { - ss = new SimpleSchema(ss); - } - - 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 ]; - - if (typeof options.selector === "object") { - // Selector Schemas - - // Extend selector schema with base schema - const baseSchema = obj._c2._simpleSchemas[0]; - if (baseSchema) { - ss = extendSchema(baseSchema.schema, ss); - } - - // Index of existing schema with identical selector - 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; - } - - 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, - }); - } 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; - } else { - // Extend existing selector schema with new selector schema. - obj._c2._simpleSchemas[schemaIndex].schema = extendSchema(obj._c2._simpleSchemas[schemaIndex].schema, ss); - } - } - } else { - // Base Schema - if (options.replace === true) { - // Replace base schema and delete all other schemas - obj._c2._simpleSchemas = [{ - schema: ss, - 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 }; - } - // 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); - } - }); - } - } - } - - attachTo(this); - // Attach the schema to the underlying LocalCollection, too - if (this._collection instanceof LocalCollection) { - this._collection._c2 = this._collection._c2 || {}; - attachTo(this._collection); - } - - defineDeny(this, options); - keepInsecure(this); - - 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 - * `selector` in args - * @param {Object} doc - It could be on update/upsert or document - * itself on insert/remove - * @param {Object} [options] - It could be on update/upsert etc - * @param {Object} [query] - it could be on update/upsert - * @return {Object} Schema - */ - obj.prototype.simpleSchema = function (doc, options, query) { - if (!this._c2) return null; - if (this._c2._simpleSchema) return this._c2._simpleSchema; - - const schemas = this._c2._simpleSchemas; - if (schemas && schemas.length > 0) { - - 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]; - - // We will set this to undefined because in theory you might want to select - // on a null value. - 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]; - } else if (typeof doc[selector] !== 'undefined') { - target = doc[selector]; - } else if (options && options.selector) { - target = options.selector[selector]; - } else if (query && query[selector]) { // on upsert/update operations - target = query[selector]; - } - - // we need to compare given selector with doc property or option to - // find right schema - if (target !== undefined && target === schema.selector[selector]) { - return schema.schema; - } - } - if (schemas[0]) { - return schemas[0].schema; - } else { - throw new Error("No default schema"); - } - } - - return null; - }; -}); - -// 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]; - - // Support missing options arg - if (!options || typeof options === "function") { - options = {}; - } - - if (this._c2 && options.bypassCollection2 !== true) { - let userId = null; - try { // https://github.com/aldeed/meteor-collection2/issues/175 - userId = Meteor.userId(); - } catch (err) {} - - args = doValidate( - this, - methodName, - args, - Meteor.isServer || this._connection === null, // getAutoValues - userId, - Meteor.isServer // isFromTrustedCode - ); - 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 methodName === "insert" ? 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); - } - - return _super.apply(this, args); - }; -}); - -/* - * Private - */ - -function doValidate(collection, type, args, getAutoValues, userId, isFromTrustedCode) { - let doc, callback, error, options, isUpsert, selector, last, hasCallback; - - if (!args.length) { - 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]; - - // The real insert doesn't take options - if (typeof options === "function") { - args = [doc, options]; - } else if (typeof callback === "function") { - args = [doc, callback]; - } else { - args = [doc]; - } - } else if (type === "update") { - selector = args[0]; - doc = args[1]; - options = args[2]; - callback = args[3]; - } else { - throw new Error("invalid type argument"); - } - - const validatedObjectWasInitiallyEmpty = isEmpty(doc); - - // Support missing options arg - if (!callback && typeof options === "function") { - callback = options; - options = {}; - } - options = options || {}; - - last = args.length - 1; - - hasCallback = (typeof args[last] === 'function'); - - // If update was called with upsert:true, flag as an upsert - isUpsert = (type === "update" && 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); - - // 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; - } - - // 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; - - if (picks && omits) { - // Pick and omit cannot both be present in the options - throw new Error('pick and omit options are mutually exclusive'); - } else if (picks) { - schema = schema.pick(...picks); - } else if (omits) { - schema = schema.omit(...omits); - } - - // Determine validation context - let validationContext = options.validationContext; - if (validationContext) { - if (typeof validationContext === 'string') { - validationContext = schema.namedContext(validationContext); - } - } else { - validationContext = schema.namedContext(); - } - - // Add a default callback function if we're on the client and no callback was given - if (Meteor.isClient && !callback) { - // 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 - // down. - callback = function(err) { - if (err) { - 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); - } - - const schemaAllowsId = schema.allowsKey("_id"); - if (type === "insert" && !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; - } - - // If _id has already been added, remove it temporarily if it's - // not explicitly defined in the schema. - let cachedId; - if (doc._id && !schemaAllowsId) { - cachedId = doc._id; - delete doc._id; - } - - const autoValueContext = { - isInsert: (type === "insert"), - isUpdate: (type === "update" && options.upsert !== true), - isUpsert, - userId, - isFromTrustedCode, - docId, - isLocalCollection - }; - - const extendAutoValueContext = { - ...((schema._cleanOptions || {}).extendAutoValueContext || {}), - ...autoValueContext, - ...options.extendAutoValueContext, - }; - - const cleanOptionsForThisOperation = {}; - ["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"), - // Start with some Collection2 defaults, which will usually be overwritten - ...Collection2.cleanOptions, - // The extend 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 - }); - - // 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) { - // 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]; - } - } - - // On the server, upserts are possible; SimpleSchema handles upserts pretty - // well by default, but it will not know about the fields in the selector, - // which are also stored in the database if an insert is performed. So we - // will allow these fields to be considered for validation by adding them - // to the $set in the modifier, while stripping out query selectors as these - // don't make it into the upserted document and break validation. - // 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); - - 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, - // we will add them to docToValidate for validation purposes only. - // This is because we want all actual values generated on the server. - if (Meteor.isClient && !isLocalCollection) { - schema.clean(docToValidate, { - autoConvert: false, - extendAutoValueContext, - filter: false, - getAutoValues: true, - isModifier: (type !== "insert"), - mutate: true, // Clean the doc/modifier in place - removeEmptyStrings: false, - removeNullsFromArrays: 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'); - } - - // Validate doc - let isValid; - if (options.validate === false) { - isValid = true; - } else { - isValid = validationContext.validate(docToValidate, { - modifier: (type === "update" || type === "upsert"), - upsert: isUpsert, - extendedCustomContext: { - isInsert: (type === "insert"), - isUpdate: (type === "update" && options.upsert !== true), - isUpsert, - userId, - isFromTrustedCode, - docId, - isLocalCollection, - ...(options.extendedCustomContext || {}), - }, - }); - } - - if (isValid) { - // Add the ID back - if (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; - } else { - args[1] = doc; - } - - // If callback, set invalidKey when we get a mongo unique error - if (Meteor.isServer && hasCallback) { - args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]); - } - - return args; - } else { - 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); - } else { - throw error; - } - } -} - -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); - - // 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; - } else { - message = `${firstErrorMessage} (${firstErrorKey})`; - } - } else { - message = "Failed validation"; - } - 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)); - } - return error; -} - -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'; - context[addValidationErrorsPropName]([{ - name: name, - type: 'notUnique', - value: val - }]); -} - -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); - } - 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); - }; -} - -let 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; - }, - update: function() { - return true; - }, - remove: function () { - return true; - }, - fetch: [], - transform: null - }); - 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 - // user will still be required to add at least one allow function of her - // own for each operation for this collection. And the user may still add - // additional deny functions, but does not have to. -} - -let alreadyDefined = {}; -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 - // 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) { - // Referenced doc is cleaned in place - c.simpleSchema(doc).clean(doc, { - mutate: true, - isModifier: false, - // We don't do these here because they are done on the client if desired - filter: false, - autoConvert: false, - removeEmptyStrings: false, - trimStrings: false, - extendAutoValueContext: { - isInsert: true, - isUpdate: false, - isUpsert: false, - userId: userId, - isFromTrustedCode: false, - docId: doc._id, - isLocalCollection: isLocalCollection - } - }); - - return false; - }, - update: function(userId, doc, fields, modifier) { - // Referenced modifier is cleaned in place - c.simpleSchema(modifier).clean(modifier, { - mutate: true, - isModifier: true, - // We don't do these here because they are done on the client if desired - filter: false, - autoConvert: false, - removeEmptyStrings: false, - trimStrings: false, - extendAutoValueContext: { - isInsert: false, - isUpdate: true, - isUpsert: false, - userId: userId, - isFromTrustedCode: false, - docId: doc && doc._id, - isLocalCollection: isLocalCollection - } - }); - - return false; - }, - fetch: ['_id'], - transform: null - }); - - // 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 - doValidate( - c, - "insert", - [ - doc, - { - trimStrings: false, - removeEmptyStrings: false, - filter: false, - autoConvert: false - }, - function(error) { - if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); - } - } - ], - false, // getAutoValues - userId, - false // isFromTrustedCode - ); - - return false; - }, - 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 - doValidate( - c, - "update", - [ - {_id: doc && doc._id}, - modifier, - { - trimStrings: false, - removeEmptyStrings: false, - filter: false, - autoConvert: false - }, - function(error) { - if (error) { - throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); - } - } - ], - false, // getAutoValues - userId, - false // isFromTrustedCode - ); - - return false; - }, - fetch: ['_id'], - ...(options.transform === true ? {} : {transform: null}), - }); - - // 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; - } -} - -function extendSchema(s1, s2) { - if (s2.version >= 2) { - const ss = new SimpleSchema(s1); - ss.extend(s2); - return ss; - } else { - return new SimpleSchema([ s1, s2 ]); - } -} +Collection2 = new EventEmitter(); -export default Collection2; +export default Collection2; \ No newline at end of file diff --git a/package/collection2/dynamic.js b/package/collection2/dynamic.js new file mode 100644 index 0000000..9384f8a --- /dev/null +++ b/package/collection2/dynamic.js @@ -0,0 +1,3 @@ +Collection2.load = function () { + import './main'; +} \ No newline at end of file diff --git a/package/collection2/lib.js b/package/collection2/lib.js index 057195d..cade2b8 100644 --- a/package/collection2/lib.js +++ b/package/collection2/lib.js @@ -1,31 +1,41 @@ export function flattenSelector(selector) { - // If selector uses $and format, convert to plain object selector + // If the selector uses $and format, convert to plain object selector if (Array.isArray(selector.$and)) { - selector.$and.forEach(sel => { + selector.$and.forEach((sel) => { Object.assign(selector, flattenSelector(sel)); }); - delete selector.$and + delete selector.$and; } - const obj = {} + const obj = {}; 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 + 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("$")))) { - obj[key] = value + obj[key] = value.$in[0]; + } else if (Object.keys(value).every((v) => !(typeof v === 'string' && v.startsWith('$')))) { + obj[key] = value; } } else { - obj[key] = value + obj[key] = value; } } - }) - - return obj + }); + + 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/main.js b/package/collection2/main.js new file mode 100644 index 0000000..fd8e968 --- /dev/null +++ b/package/collection2/main.js @@ -0,0 +1,876 @@ +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import SimpleSchema from "meteor/aldeed:simple-schema"; +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'; + + +/** + * Mongo.Collection.prototype.attachSchema + * @param {SimpleSchema|Object} ss - SimpleSchema instance or a schema definition object + * from which to create a new SimpleSchema instance + * @param {Object} [options] + * @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, + * such as Meteor.users. It is most likely unsafe to call this method more than + * 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 || {}; + + // Allow passing just the schema object + if (!SimpleSchema.isSimpleSchema(ss)) { + ss = new SimpleSchema(ss); + } + + 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]; + + if (typeof options.selector === 'object') { + // Selector Schemas + + // Extend selector schema with base schema + const baseSchema = obj._c2._simpleSchemas[0]; + if (baseSchema) { + ss = extendSchema(baseSchema.schema, ss); + } + + // Index of existing schema with identical selector + let schemaIndex; + + // Loop through existing schemas with selectors, + 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 + }); + } 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; + } else { + // Extend existing selector schema with new selector schema. + obj._c2._simpleSchemas[schemaIndex].schema = extendSchema( + obj._c2._simpleSchemas[schemaIndex].schema, + ss + ); + } + } + } else { + // Base Schema + if (options.replace === true) { + // Replace base schema and delete all other schemas + obj._c2._simpleSchemas = [ + { + schema: ss, + selector: options.selector + } + ]; + } else { + // Set base schema if not yet set + if (!obj._c2._simpleSchemas[0]) { + 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 + ); + } + }); + } + } + } + + attachTo(this); + // Attach the schema to the underlying LocalCollection, too + if (this._collection instanceof LocalCollection) { + this._collection._c2 = this._collection._c2 || {}; + attachTo(this._collection); + } + + defineDeny(this, options); + keepInsecure(this); + + Collection2.emit('schema.attached', this, ss, options); + }; + + [Mongo.Collection, LocalCollection].forEach((obj) => { + /** + * simpleSchema + * @description function detect the correct schema by given params. If it + * 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 + * @param {Object} [options] - It could be on update/upsert etc + * @param {Object} [query] - it could be on update/upsert + * @return {Object} Schema + */ + obj.prototype.simpleSchema = function (doc, options, query) { + if (!this._c2) return null; + if (this._c2._simpleSchema) return this._c2._simpleSchema; + + const schemas = this._c2._simpleSchemas; + if (schemas && schemas.length > 0) { + let schema, selector, target; + // Position 0 reserved for base schema + 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 + // on a null value. + 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]; + } else if (typeof doc[selector] !== 'undefined') { + target = doc[selector]; + } else if (options && options.selector) { + target = options.selector[selector]; + } else if (query && query[selector]) { + // on upsert/update operations + target = query[selector]; + } + + // we need to compare given selector with doc property or option to + // find the right schema + if (target !== undefined && target === schema.selector[selector]) { + return schema.schema; + } + } + if (schemas[0]) { + return schemas[0].schema; + } else { + throw new Error('No default schema'); + } + } + + return null; + }; + }); + + function _methodMutation(async, methodName) { + const _super = Meteor.isFibersDisabled + ? Mongo.Collection.prototype[methodName] + : Mongo.Collection.prototype[methodName.replace('Async', '')]; + + 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 = {}; + } + + let validationContext = {}; + let error; + 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 + async + ); + + 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 (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, err.code); + 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 = {}; + } + + 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, err.code); + } + }; + } + + // 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, async) { + let doc, callback, error, options, selector; + + if (!args.length) { + throw new Error(type + ' requires an argument'); + } + + // Gather arguments and cache the selector + 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]; + } else { + args = [doc]; + } + } else if (isUpdateType(type)) { + selector = args[0]; + doc = args[1]; + options = args[2]; + callback = args[3]; + } else { + throw new Error('invalid type argument'); + } + + const validatedObjectWasInitiallyEmpty = isEmpty(doc); + + // Support missing options arg + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + + const last = args.length - 1; + + const hasCallback = typeof args[last] === 'function'; + + // If update was called with upsert:true, flag as an upsert + 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; + + // 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; + } + + // 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; + + if (picks && omits) { + // Pick and omit cannot both be present in the options + throw new Error('pick and omit options are mutually exclusive'); + } else if (picks) { + schema = schema.pick(...picks); + } else if (omits) { + schema = schema.omit(...omits); + } + + // Determine validation context + let validationContext = options.validationContext; + if (validationContext) { + if (typeof validationContext === 'string') { + validationContext = schema.namedContext(validationContext); + } + } else { + validationContext = schema.namedContext(); + } + + // Add a default callback function if we're on the client and no callback was given + 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 writing doesn't work because their database is + // down. + callback = function (err) { + if (err) { + 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); + } + + 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 (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; + if (doc._id && !schemaAllowsId) { + cachedId = doc._id; + delete doc._id; + } + + const autoValueContext = { + isInsert: isInsertType(type), + isUpdate: isUpdateType(type) && options.upsert !== true, + isUpsert, + userId, + isFromTrustedCode, + docId, + isLocalCollection + }; + + const extendAutoValueContext = { + ...((schema._cleanOptions || {}).extendAutoValueContext || {}), + ...autoValueContext, + ...options.extendAutoValueContext + }; + + const cleanOptionsForThisOperation = {}; + ['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: !isInsertType(type), + // 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 + }); + + // 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. + 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]; + } + } + + // On the server, upserts are possible; SimpleSchema handles upserts pretty + // well by default, but it will not know about the fields in the selector, + // which are also stored in the database if an insert is performed. So we + // will allow these fields to be considered for validation by adding them + // to the $set in the modifier, while stripping out query selectors as these + // don't make it into the upserted document and break validation. + // 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); + + 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, + // we will add them to docToValidate for validation purposes only. + // This is because we want all actual values generated on the server. + if (Meteor.isClient && !isLocalCollection) { + schema.clean(docToValidate, { + autoConvert: false, + extendAutoValueContext, + filter: false, + getAutoValues: true, + isModifier: !isInsertType(type), + mutate: true, // Clean the doc/modifier in place + removeEmptyStrings: false, + removeNullsFromArrays: 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 ' + + (isUpdateType(type) ? 'modifier' : 'object') + + ' is now empty' + ); + } + + // Validate doc + let isValid; + if (options.validate === false) { + isValid = true; + } else { + isValid = validationContext.validate(docToValidate, { + modifier: isUpdateType(type) || isUpsertType(type), + upsert: isUpsert, + extendedCustomContext: { + isInsert: isInsertType(type), + isUpdate: isUpdateType(type) && options.upsert !== true, + isUpsert, + userId, + isFromTrustedCode, + docId, + isLocalCollection, + ...(options.extendedCustomContext || {}) + } + }); + } + + if (isValid) { + // Add the ID back + if (cachedId) { + doc._id = cachedId; + } + + // Update the args to reflect the cleaned doc + // XXX not sure if this is necessary since we mutate + if (isInsertType(type)) { + args[0] = doc; + } else { + args[1] = doc; + } + + // If callback, set invalidKey when we get a mongo unique error + if (Meteor.isServer && hasCallback) { + args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]); + } + + return [args, validationContext]; + } else { + 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); + return []; + } else { + throw error; + } + } + } + + function getErrorObject(context, appendToMessage = '', code) { + 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); + + // 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; + } else { + message = `${firstErrorMessage} (${firstErrorKey})`; + } + } else { + message = 'Failed validation'; + } + message = `${message} ${appendToMessage}`.trim(); + const error = new Error(message); + error.invalidKeys = invalidKeys; + error.validationContext = context; + error.code = code; + // 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)); + } + return error; + } + + 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'; + context[addValidationErrorsPropName]([ + { + name, + type: 'notUnique', + value: val + } + ]); + } + + 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); + } + 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); + }; + } + + 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]) { + const allow = { + insert: function () { + return true; + }, + update: function () { + return true; + }, + remove: function () { + return true; + }, + fetch: [], + transform: null + }; + + 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 + // user will still be required to add at least one allow function of her + // own for each operation for this collection. And the user may still add + // additional deny functions, but does not have to. + } + + const alreadyDefined = {}; + + 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 + // and auto-values. This must be done with "transform: null" or we would be + // extending a clone of doc and therefore have no effect. + const firstDeny = { + insert: function (userId, doc) { + // Referenced doc is cleaned in place + c.simpleSchema(doc).clean(doc, { + mutate: true, + isModifier: false, + // We don't do these here because they are done on the client if desired + filter: false, + autoConvert: false, + removeEmptyStrings: false, + trimStrings: false, + extendAutoValueContext: { + isInsert: true, + isUpdate: false, + isUpsert: false, + userId, + isFromTrustedCode: false, + docId: doc._id, + isLocalCollection + } + }); + + return false; + }, + update: function (userId, doc, fields, modifier) { + // Referenced modifier is cleaned in place + c.simpleSchema(modifier).clean(modifier, { + mutate: true, + isModifier: true, + // We don't do these here because they are done on the client if desired + filter: false, + autoConvert: false, + removeEmptyStrings: false, + trimStrings: false, + extendAutoValueContext: { + isInsert: false, + isUpdate: true, + isUpsert: false, + userId, + isFromTrustedCode: false, + docId: doc && doc._id, + isLocalCollection + } + }); + + 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 + // 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. + 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', + [ + doc, + { + trimStrings: false, + removeEmptyStrings: false, + filter: false, + autoConvert: false + }, + function (error) { + if (error) { + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); + } + } + ], + false, // getAutoValues + userId, + false // isFromTrustedCode + ); + + return false; + }, + 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 the client if desired + doValidate( + c, + 'update', + [ + { _id: doc && doc._id }, + modifier, + { + trimStrings: false, + removeEmptyStrings: false, + filter: false, + autoConvert: false + }, + function (error) { + if (error) { + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); + } + } + ], + false, // getAutoValues + userId, + false // isFromTrustedCode + ); + + return false; + }, + fetch: ['_id'], + ...(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; + } + } + + function extendSchema(s1, s2) { + if (s2.version >= 2) { + const ss = new SimpleSchema(s1); + ss.extend(s2); + return ss; + } else { + return new SimpleSchema([s1, s2]); + } + } \ No newline at end of file diff --git a/package/collection2/package.js b/package/collection2/package.js index 4177ad1..b6ed067 100644 --- a/package/collection2/package.js +++ b/package/collection2/package.js @@ -1,33 +1,42 @@ -/* 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: '4.0.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']); +Package.onUse(function (api) { + api.versionsFrom(['1.12.1', '2.3', '3.0-beta.0']); 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'); + api.use('ecmascript@0.16.8-alpha300.11'); + api.use('aldeed:simple-schema@1.13.1'); - // Allow us to detect 'insecure'. - api.use('insecure@1.0.7', {weak: true}); + api.addFiles(['./collection2.js']); + api.export('Collection2', 'server'); - api.mainModule('collection2.js'); + // Allow us to detect 'insecure'. + api.use('insecure', { weak: true }); api.export('Collection2'); }); + +Package.onTest(function (api) { + api.use([ + 'meteortesting:mocha@3.1.0-beta300.0', + 'aldeed:collection2@4.0.0-beta.7' + ]) +}); \ No newline at end of file diff --git a/package/collection2/static.js b/package/collection2/static.js new file mode 100644 index 0000000..06b70fc --- /dev/null +++ b/package/collection2/static.js @@ -0,0 +1 @@ +import './main'; \ No newline at end of file diff --git a/tests/.meteor/packages b/tests/.meteor/packages index 7e11127..82e379d 100644 --- a/tests/.meteor/packages +++ b/tests/.meteor/packages @@ -4,24 +4,25 @@ # '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.2-beta300.0 # Packages every Meteor app needs to have +mongo@2.0.0-beta300.0 # The database Meteor supports right now +reactive-var@1.0.13-beta300.0 # Reactive variable for tracker jquery # Helpful client-side library -tracker@1.2.0 # Meteor's client-side reactive programming library +tracker@1.3.3-beta300.0 # 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 -es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.15.1 # Enable ECMAScript2015+ syntax in app code -shell-server@0.5.0 # Server-side component of the `meteor shell` command +standard-minifier-css@1.9.3-beta300.0 # CSS minifier run for production mode +standard-minifier-js@3.0.0-beta300.0 # JS minifier run for production mode +es5-shim@4.8.1-beta300.0 # ECMAScript 5 compatibility for older browsers. +ecmascript@0.16.8-beta300.0 # Enable ECMAScript2015+ syntax in app code +shell-server@0.6.0-beta300.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) +autopublish@1.0.8-beta300.0 # Publish all data to the clients (for prototyping) +insecure@1.0.8-beta300.0 # Allow all DB writes from clients (for prototyping) -underscore@1.0.10 -dynamic-import@0.6.0 -aldeed:collection2 -meteortesting:mocha +underscore@1.0.14-beta300.0 +dynamic-import@0.7.4-beta300.0 + +aldeed:simple-schema@1.13.1 +aldeed:collection2@4.0.0-beta.7 +meteortesting:mocha@3.1.0-beta300.0 +meteortesting:mocha-core@8.3.0-beta300.0 diff --git a/tests/.meteor/release b/tests/.meteor/release index d8fd7cf..3952621 100644 --- a/tests/.meteor/release +++ b/tests/.meteor/release @@ -1 +1 @@ -METEOR@2.2 +METEOR@3.0-beta.0 diff --git a/tests/.meteor/versions b/tests/.meteor/versions index c9986aa..59bdfa9 100644 --- a/tests/.meteor/versions +++ b/tests/.meteor/versions @@ -1,87 +1,69 @@ -aldeed:collection2@3.4.1 -allow-deny@1.1.0 -autopublish@1.0.7 -autoupdate@1.7.0 -babel-compiler@7.6.1 -babel-runtime@1.5.0 -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 -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 -es5-shim@4.8.0 -fetch@0.1.1 -geojson-utils@1.0.10 -hot-code-push@1.0.4 -html-tools@1.1.1 -htmljs@1.1.0 -http@1.4.4 -id-map@1.1.1 -insecure@1.0.7 -inter-process-messaging@0.1.1 +aldeed:collection2@4.0.0 +aldeed:simple-schema@1.13.1 +allow-deny@2.0.0-beta300.0 +autopublish@1.0.8-beta300.0 +autoupdate@2.0.0-beta300.0 +babel-compiler@7.11.0-beta300.0 +babel-runtime@1.5.2-beta300.0 +base64@1.0.13-beta300.0 +binary-heap@1.0.12-beta300.0 +boilerplate-generator@2.0.0-beta300.0 +callback-hook@1.6.0-beta300.0 +check@1.3.3-beta300.0 +core-runtime@1.0.0-beta300.0 +ddp@1.4.2-beta300.0 +ddp-client@3.0.0-beta300.0 +ddp-common@1.4.1-beta300.0 +ddp-server@3.0.0-beta300.0 +diff-sequence@1.1.3-beta300.0 +dynamic-import@0.7.4-beta300.0 +ecmascript@0.16.8-beta300.0 +ecmascript-runtime@0.8.2-beta300.0 +ecmascript-runtime-client@0.12.2-beta300.0 +ecmascript-runtime-server@0.11.1-beta300.0 +ejson@1.1.4-beta300.0 +es5-shim@4.8.1-beta300.0 +facts-base@1.0.2-beta300.0 +fetch@0.1.4-beta300.0 +geojson-utils@1.0.12-beta300.0 +hot-code-push@1.0.5-beta300.0 +http@1.0.1 +id-map@1.2.0-beta300.0 +insecure@1.0.8-beta300.0 +inter-process-messaging@0.1.2-beta300.0 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 -mongo-dev-server@1.1.0 -mongo-id@1.0.8 -npm-mongo@3.9.0 -observe-sequence@1.0.16 -ordered-dict@1.1.0 -promise@0.11.2 +logging@1.3.3-beta300.0 +meteor@2.0.0-beta300.0 +meteor-base@1.5.2-beta300.0 +meteortesting:browser-tests@1.6.0-beta300.0 +meteortesting:mocha@2.1.0 +meteortesting:mocha-core@8.3.1-beta300.0 +minifier-css@2.0.0-beta300.0 +minifier-js@3.0.0-beta300.0 +minimongo@2.0.0-beta300.0 +modern-browsers@0.1.10-beta300.0 +modules@0.19.1-beta300.0 +modules-runtime@0.13.2-beta300.0 +mongo@2.0.0-beta300.0 +mongo-decimal@0.1.4-beta300.0 +mongo-dev-server@1.1.1-beta300.0 +mongo-id@1.0.9-beta300.0 +npm-mongo@4.16.1-beta300.0 +ordered-dict@1.2.0-beta300.0 +promise@1.0.0-beta300.0 raix:eventemitter@1.0.0 -random@1.2.0 -react-fast-refresh@0.1.1 -reactive-var@1.0.11 -reload@1.3.1 -retry@1.1.0 -routepolicy@1.1.0 -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 +random@1.2.2-beta300.0 +react-fast-refresh@0.2.8-beta300.0 +reactive-var@1.0.13-beta300.0 +reload@1.3.2-beta300.0 +retry@1.1.1-beta300.0 +routepolicy@1.1.2-beta300.0 +shell-server@0.6.0-beta300.0 +socket-stream-client@0.5.2-beta300.0 +standard-minifier-css@1.9.3-beta300.0 +standard-minifier-js@3.0.0-beta300.0 +tracker@1.3.3-beta300.0 +typescript@4.9.5-beta300.0 +underscore@1.0.14-beta300.0 +webapp@2.0.0-beta300.0 +webapp-hashing@1.1.2-beta300.0 diff --git a/tests/_main.tests.js b/tests/_main.tests.js deleted file mode 100644 index b012711..0000000 --- a/tests/_main.tests.js +++ /dev/null @@ -1 +0,0 @@ -import 'babel-polyfill'; diff --git a/tests/autoValue.tests.js b/tests/autoValue.tests.js index ca8ff95..9bdcea3 100644 --- a/tests/autoValue.tests.js +++ b/tests/autoValue.tests.js @@ -1,36 +1,50 @@ +import 'meteor/aldeed:collection2/dynamic'; import { Meteor } from 'meteor/meteor'; import expect from 'expect'; import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import SimpleSchema from "meteor/aldeed:simple-schema"; +import { callMongoMethod } from './helper'; + +/* global describe, it */ + +Collection2.load(); const collection = new Mongo.Collection('autoValueTestCollection'); -const localCollection = new Mongo.Collection('autoValueTestLocalCollection', { connection: null }); +const localCollection = new Mongo.Collection('autoValueTestLocalCollection', { + connection: null +}); [collection, localCollection].forEach((c) => { - c.attachSchema(new SimpleSchema({ - clientAV: { - type: SimpleSchema.Integer, - optional: true, - 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; + c.attachSchema( + new SimpleSchema({ + clientAV: { + type: SimpleSchema.Integer, + optional: true, + 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; + } } - } - })); + }) + ); }); 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) => { + if (error) { + done(error); + return; + } const doc = collection.findOne(id); expect(doc.clientAV).toBe(undefined); expect(doc.serverAV).toBe(1); @@ -40,6 +54,10 @@ if (Meteor.isClient) { it('runs function once for LocalCollection', function (done) { localCollection.insert({}, (error, id) => { + if (error) { + done(error); + return; + } const doc = localCollection.findOne(id); expect(doc.clientAV).toBe(1); expect(doc.serverAV).toBe(undefined); @@ -49,6 +67,10 @@ if (Meteor.isClient) { it('with getAutoValues false, does not run function for LocalCollection', function (done) { localCollection.insert({}, { getAutoValues: false }, (error, id) => { + if (error) { + done(error); + return; + } const doc = localCollection.findOne(id); expect(doc.clientAV).toBe(undefined); expect(doc.serverAV).toBe(undefined); @@ -60,30 +82,30 @@ if (Meteor.isClient) { if (Meteor.isServer) { describe('autoValue on server', function () { - it('runs function once', function () { - const id = collection.insert({}); - const doc = collection.findOne(id); + 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); + 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); + 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); + 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..174997a 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 SimpleSchema from "meteor/aldeed:simple-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,11 +48,11 @@ const booksSchema = new SimpleSchema({ }, createdAt: { type: Date, - optional: true, + optional: true }, updatedAt: { type: Date, - optional: true, + optional: true } }); @@ -55,55 +60,320 @@ const books = new Mongo.Collection('books'); books.attachSchema(booksSchema); const upsertTest = new Mongo.Collection('upsertTest'); -upsertTest.attachSchema(new SimpleSchema({ - _id: {type: String}, - foo: {type: Number} -})); +upsertTest.attachSchema( + new SimpleSchema({ + _id: { type: String }, + foo: { type: Number } + }) +); export default function addBooksTests() { describe('insert', function () { - beforeEach(function () { - books.find({}).forEach(book => { - books.remove(book._id); - }); + beforeEach(async function () { + for (const book of await callMeteorFetch(books, {})) { + await callMongoMethod(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'); + 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(); }); - 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", - copies: 1, - updatedAt: new Date() - }, (error) => { - expect(error).toBe(null); - done(); - }); + callMongoMethod(books._collection, 'insert', [ + { + title: 'Ulysses', + author: 'James Joyce', + copies: 1, + updatedAt: new Date() + } + ]) + .then(() => { + done(); + }) + .catch(done); }); } }); @@ -111,8 +381,7 @@ export default function addBooksTests() { if (Meteor.isServer) { describe('upsert', function () { function getCallback(done) { - return (error, result) => { - expect(!!error).toBe(false); + return (result) => { expect(result.numberAffected).toBe(1); const validationErrors = books.simpleSchema().namedContext().validationErrors(); @@ -123,9 +392,7 @@ export default function addBooksTests() { } function getUpdateCallback(done) { - return (error, result) => { - if (error) console.error(error); - expect(!!error).toBe(false); + return (result) => { expect(result).toBe(1); const validationErrors = books.simpleSchema().namedContext().validationErrors(); @@ -136,9 +403,9 @@ export default function addBooksTests() { } function getErrorCallback(done) { - return (error, result) => { + return (error) => { expect(!!error).toBe(true); - expect(!!result).toBe(false); + // expect(!!result).toBe(false) const validationErrors = books.simpleSchema().namedContext().validationErrors(); expect(validationErrors.length).toBe(1); @@ -148,275 +415,325 @@ export default function addBooksTests() { } it('valid', function (done) { - books.upsert({ - title: "Ulysses", - author: "James Joyce" - }, { - $set: { - title: "Ulysses", - author: "James Joyce", - copies: 1 + callMongoMethod(books, 'upsert', [ + { + title: 'Ulysses', + author: 'James Joyce' + }, + { + $set: { + 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" - }, { - $set: { - title: "Ulysses", - author: "James Joyce", - copies: 1 + callMongoMethod(books, 'update', [ + { + title: 'Ulysses', + author: 'James Joyce' + }, + { + $set: { + title: 'Ulysses', + author: 'James Joyce', + copies: 1 + } + }, + { + upsert: true } - }, { - upsert: true - }, getUpdateCallback(done)); + ]) + .then(getUpdateCallback(done)) + .catch(done); }); it('upsert as update with $and', function (done) { - books.update({ - $and: [ - { title: "Ulysses" }, - { author: "James Joyce" }, - ], - }, { - $set: { - title: "Ulysses", - author: "James Joyce", - copies: 1 + callMongoMethod(books, 'update', [ + { + $and: [{ title: 'Ulysses' }, { author: 'James Joyce' }] + }, + { + $set: { + title: 'Ulysses', + author: 'James Joyce', + copies: 1 + } + }, + { + upsert: true } - }, { - upsert: true - }, getUpdateCallback(done)); + ]) + .then(getUpdateCallback(done)) + .catch(done); }); it('upsert - invalid', function (done) { - books.upsert({ - title: "Ulysses", - author: "James Joyce" - }, { - $set: { - copies: -1 + 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" - }, { - $set: { - copies: -1 + callMongoMethod(books, 'update', [ + { + title: 'Ulysses', + author: 'James Joyce' + }, + { + $set: { + copies: -1 + } + }, + { + upsert: true } - }, { - 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" - }, { - $set: { - copies: 1 + 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" - }, { - $set: { - copies: 1 + callMongoMethod(books, 'update', [ + { + title: 'Ulysses', + author: 'James Joyce' + }, + { + $set: { + copies: 1 + } + }, + { + upsert: true } - }, { - 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); - } - - // do a good one to set up update test - books.insert({ - title: title + " 2", - author: "James Joyce", - copies: 1 - }, { - validate: false, - validationContext: "validateFalse2" - }, (error, newId) => { - const validationErrors = books.simpleSchema().namedContext("validateFalse2").validationErrors(); - - expect(!!error).toBe(false); - expect(!!newId).toBe(true); - expect(validationErrors.length).toBe(0); - - const insertedBook = books.findOne({ title: title + " 2" }); - expect(!!insertedBook).toBe(true); - - books.update({ - _id: newId - }, { - $set: { - copies: "Yes Please" - } - }, { + if (Meteor.isServer) { + it('validate false', function (done) { + const title = 'Validate False Server'; + let insertedBook, error, newId; + + callMongoMethod(books, 'insert', [ + { + title, + author: 'James Joyce' + }, + { validate: false, - validationContext: "validateFalse3" - }, (error, result) => { - let updatedBook; - const validationErrors = books.simpleSchema().namedContext("validateFalse3").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); - 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); + 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; - 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'); - } + const validationErrors = books + .simpleSchema() + .namedContext('validateFalse2') + .validationErrors(); - // now try a good one - books.update({ - _id: newId - }, { - $set: { - copies: 3 + expect(!!newId).toBe(true); + expect(validationErrors.length).toBe(0); + + return callMongoMethod(books, 'findOne', [{ title: title + ' 2' }]); + }) + .then((insertedBook) => { + expect(!!insertedBook).toBe(true); + + return callMongoMethod(books, 'update', [ + { + _id: newId + }, + { + $set: { + copies: 'Yes Please' + } + }, + { + validate: false, + validationContext: 'validateFalse3' } - }, { - 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(); - }); - }); - }); + ]); + }) + .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; - - try { - id = books.insert({}, {bypassCollection2: true}) - } catch (error) { - done(error); - } - - try { - books.update(id, {$set: {copies: 2}}, {bypassCollection2: true}) - done(); - } catch (error) { - done(error); - } + it('bypassCollection2 5', async function () { + const id = await callMongoMethod(books, 'insert', [{}, { bypassCollection2: true }]); + + await callMongoMethod(books, 'update', [ + id, + { $set: { copies: 2 } }, + { bypassCollection2: true } + ]); }); - it('everything filtered out', function () { - expect(function () { - upsertTest.update({_id: '123'}, { - $set: { - boo: 1 + it('everything filtered out', async function () { + try { + 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}, { - $set: { - foo: 2 + await callMongoMethod(upsertTest, 'update', [ + { _id: upsertTestId }, + { + $set: { + foo: 2 + } + }, + { + upsert: true } - }, { - upsert: true - }); - const doc = upsertTest.findOne(upsertTestId); + ]); + + 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..c2d8f2c 100644 --- a/tests/clean.tests.js +++ b/tests/clean.tests.js @@ -1,6 +1,10 @@ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import SimpleSchema from "meteor/aldeed:simple-schema"; +import { Meteor } from 'meteor/meteor'; +import { callMongoMethod } from './helper'; + +/* global describe it */ let collection; @@ -13,37 +17,49 @@ if (Meteor.isClient) { describe('clean options', function () { describe('filter', function () { it('keeps default schema clean options', function (done) { - const schema = new SimpleSchema({ - name: String, - }, { - clean: { - filter: false, + const schema = new SimpleSchema( + { + name: String }, - }); + { + clean: { + filter: false + } + } + ); 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, - }, { - clean: { - filter: true, + const schema = new SimpleSchema( + { + name: String }, - }); + { + clean: { + filter: 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) { @@ -51,46 +67,61 @@ describe('clean options', function () { 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, - }, { - clean: { - autoConvert: false, + const schema = new SimpleSchema( + { + name: String }, - }); + { + clean: { + autoConvert: false + } + } + ); 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, - }, { - clean: { - autoConvert: true, + const schema = new SimpleSchema( + { + name: String }, - }); + { + clean: { + autoConvert: 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) { @@ -98,48 +129,63 @@ describe('clean options', function () { 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) { - const schema = new SimpleSchema({ - name: String, - other: Number - }, { - clean: { - removeEmptyStrings: false, + const schema = new SimpleSchema( + { + name: String, + other: Number }, - }); + { + clean: { + removeEmptyStrings: false + } + } + ); 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, - }, { - clean: { - removeEmptyStrings: true, + const schema = new SimpleSchema( + { + name: String, + other: Number }, - }); + { + clean: { + removeEmptyStrings: 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) { @@ -147,48 +193,65 @@ describe('clean options', function () { 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, - }, { - clean: { - trimStrings: false, + const schema = new SimpleSchema( + { + 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(); - }); + 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, - }, { - clean: { - trimStrings: true, + const schema = new SimpleSchema( + { + 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(); - }); + 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) { @@ -196,11 +259,15 @@ describe('clean options', function () { collection.attachSchema(schema, { replace: true }); - collection.insert({ name: ' foo ' }, (error, _id) => { - expect(error).toBe(null); - expect(collection.findOne(_id)).toEqual({ _id, name: 'foo' }); - done(); - }); + 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..5767697 100644 --- a/tests/collection2.tests.js +++ b/tests/collection2.tests.js @@ -1,20 +1,22 @@ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; -import { _ } from 'meteor/underscore'; - +import SimpleSchema from 'meteor/aldeed:simple-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 } }) ); @@ -22,325 +24,319 @@ describe('collection2', function () { }); 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); }); - 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'; - 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'); + 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'; - }, - }, + } + } }) ); - upsertAutoValueTest.remove({}); + await callMongoMethod(upsertAutoValueTest, 'remove', [{}]); - upsertAutoValueTest.upsert( + await callMongoMethod(upsertAutoValueTest, 'upsert', [ { - foo: 'bar', + foo: 'bar' }, { $set: { - av: 'abc', - }, - }, - (error, result) => { - expect(times).toBe(1); - done(); + 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({}); + 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( - { - foo: { $gte: yesterday, $lte: tomorrow }, - }, - { - $set: { - bar: 2, - }, - $inc: { - baz: 4, + const { numberAffected, insertedId } = await callMongoMethod( + upsertQueryOperatorsTest, + 'upsert', + [ + { + foo: { $gte: yesterday, $lte: tomorrow } }, - } + { + $set: { + bar: 2 + }, + $inc: { + baz: 4 + } + } + ] ); expect(numberAffected).toBe(1); - const doc = upsertQueryOperatorsTest.findOne(); + + 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( + const result = await callMongoMethod(upsertQueryOperatorUndefinedTest, 'upsert', [ { - foo: undefined, + foo: undefined }, { $set: { - bar: 2, + bar: 2 }, $inc: { - baz: 4, - }, - }, - (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); + baz: 4 + } + } + ]); - 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); + expect(result.numberAffected).toBe(1); - done(); - } - ); + 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 + }, + $inc: { + baz: 4 + } } - ); + ]); + + expect(result2.numberAffected).toBe(1); + + 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', function (done) { + 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( @@ -348,56 +344,56 @@ describe('collection2', function () { 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( + + const result = await callMongoMethod(upsertQueryOperatorNestedObject, 'upsert', [ { - test: '1', + test: '1' }, { $set: { foo: { bar: '1', - baz: '2', + baz: '2' }, - test: testDateValue, - }, - }, - (error, result) => { - expect(error).toBe(null); - expect(result.numberAffected).toBe(1); - - const doc = upsertQueryOperatorNestedObject.findOne({ - _id: result.insertedId, - }); + test: testDateValue + } + } + ]); - expect(result.insertedId).toBe(doc._id); - expect(doc.foo.bar).toBe('1'); - expect(doc.foo.baz).toBe('2'); - expect(doc.test).toEqual(testDateValue); + expect(result.numberAffected).toBe(1); - done(); + const doc = await callMongoMethod(upsertQueryOperatorNestedObject, 'findOne', [ + { + _id: result.insertedId } - ); + ]); + + 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( @@ -406,285 +402,288 @@ 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; }, + connection: Meteor.isClient ? null : undefined }); - noSchemaCollection.insert( + const newId = await callMongoMethod(noSchemaCollection, 'insert', [ { a: 1, - b: 2, - }, - (error, newId) => { - expect(!!error).toBe(false); - expect(!!newId).toBe(true); + b: 2 + } + ]); - const doc = noSchemaCollection.findOne(newId); - expect(doc instanceof Object).toBe(true); - expect(doc.userFoo).toBe('userBar'); + expect(!!newId).toBe(true); - noSchemaCollection.update( - { - _id: newId, - }, - { - $set: { - a: 3, - b: 4, - }, - }, - (error) => { - expect(!!error).toBe(false); - done(); - } - ); + 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'); + const RES = new Mongo.Collection('RES', Meteor.isClient ? { connection: null } : undefined); RES.attachSchema(RESSchema); // Remove empty strings (default) - RES.insert( + const newId1 = await callMongoMethod(RES, 'insert', [ { foo: 'foo', - bar: '', - }, - (error, newId1) => { - expect(!!error).toBe(false); - expect(typeof newId1).toBe('string'); + bar: '' + } + ]); + expect(typeof newId1).toBe('string'); - const doc = RES.findOne(newId1); - expect(doc instanceof Object).toBe(true); - expect(doc.bar).toBe(undefined); + const doc = await callMongoMethod(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(); - } - ); - } - ); + // Don't remove empty strings + const newId2 = await callMongoMethod(RES, 'insert', [ + { + foo: 'foo', + bar: '' + }, + { + removeEmptyStrings: false } - ); - }); + ]); - it('extending a schema after attaching it, collection2 validation respects the extension', (done) => { - const schema = new SimpleSchema({ - foo: String, - }); + expect(typeof newId2).toBe('string'); - const collection = new Mongo.Collection('ExtendAfterAttach'); - collection.attachSchema(schema); + const doc2 = await callMongoMethod(RES, 'findOne', [newId2]); + expect(doc2 instanceof Object).toBe(true); + expect(doc2.bar).toBe(''); - collection.insert( + // Don't remove empty strings for an update either + const result = await callMongoMethod(RES, 'update', [ { - foo: 'foo', - bar: 'bar', + _id: newId1 }, { - filter: false, + $set: { + bar: '' + } }, - (error) => { - expect(error.invalidKeys[0].name).toBe('bar'); - schema.extend({ - bar: String, - }); + { + removeEmptyStrings: false + } + ]); - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error2) => { - expect(!!error2).toBe(false); + expect(result).toBe(1); + const doc3 = await callMongoMethod(RES, 'findOne', [newId1]); + expect(doc3 instanceof Object).toBe(true); + expect(doc3.bar).toBe(''); + }); - done(); - } - ); - } + it('extending a schema after attaching it, collection2 validation respects the extension', async function () { + const schema = new SimpleSchema({ + foo: String + }); + + const collection = new Mongo.Collection( + 'ExtendAfterAttach', + Meteor.isClient ? { connection: null } : undefined ); + collection.attachSchema(schema); + + 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 + } + ]); + + return; + } + + throw new Error('should not get here'); }); - it('extending a schema with a selector after attaching it, collection2 validation respects the extension', (done) => { + it('extending a schema with a selector after attaching it, collection2 validation respects the extension', async () => { const schema = new SimpleSchema({ - foo: String, + foo: String }); - const collection = new Mongo.Collection('ExtendAfterAttach2'); + const collection = new Mongo.Collection( + 'ExtendAfterAttach2', + Meteor.isClient ? { connection: null } : undefined + ); collection.attachSchema(schema, { selector: { foo: 'foo' } }); - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error) => { - expect(error.invalidKeys[0].name).toBe('bar'); - - schema.extend({ - bar: String, - }); + try { + await callMongoMethod(collection, 'insert', [ + { + foo: 'foo', + bar: 'bar' + }, + { + filter: false + } + ]); + } catch (error) { + expect(error.invalidKeys[0].name).toBe('bar'); - collection.insert( - { - foo: 'foo', - bar: 'bar', - }, - { - filter: false, - }, - (error2) => { - expect(!!error2).toBe(false); + schema.extend({ + bar: String + }); - done(); - } - ); - } - ); + 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'); + const collection = new Mongo.Collection( + 'pickOrOmit', + Meteor.isClient ? { connection: null } : undefined + ); collection.attachSchema(collectionSchema); - // Test error from including both pick and omit + // 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' - ); + expect(error.message).toBe('pick and omit options are mutually exclusive'); 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 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'); + }); - 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'); - } - ); - }); + it('pins code when throwing out an error', async function () { + if (Meteor.isServer) { + const testCollection = new Mongo.Collection('duplicate_code_collection'); + await testCollection.createIndexAsync({ name: 1 }, { unique: true }); + // remove any inserts on previous re-runs to avoid test failing + await testCollection.removeAsync({}); + // first insert + await testCollection.insertAsync({ name: 'foo' }); + try { + // second insert, throws out error + await testCollection.insertAsync({ name: 'foo' }); + } catch (e) { + expect(e.code).toBe(11000); + } + } }); addBooksTests(); diff --git a/tests/context.tests.js b/tests/context.tests.js index 24d6e04..6576feb 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 SimpleSchema from "meteor/aldeed:simple-schema"; +import { Meteor } from 'meteor/meteor'; +import { callMongoMethod } from './helper'; + +/* global it */ const contextCheckSchema = new SimpleSchema({ foo: { @@ -10,7 +14,7 @@ const contextCheckSchema = new SimpleSchema({ context: { type: Object, optional: true, - defaultValue: {}, + defaultValue: {} }, 'context.userId': { type: String, @@ -53,53 +57,48 @@ const contextCheck = new Mongo.Collection('contextCheck'); contextCheck.attachSchema(contextCheckSchema); export default function addContextTests() { - it('AutoValue Context', function (done) { - let testId; - - const callback1 = () => { - const ctx = contextCheck.findOne(testId); - expect(ctx.context.docId).toBe(testId); - done(); - }; + 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, { + await callMongoMethod(contextCheck, 'update', [ + { + _id: testId + }, + { $set: { context: {}, - foo: "bar" + foo: 'bar' } - }, callback1); - }; + } + ]); - 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 - }, { + // make sure docId works with `_id` direct, too + await callMongoMethod(contextCheck, 'update', [ + testId, + { $set: { context: {}, - foo: "bar" + foo: 'bar' } - }, callback2); - }; + } + ]); - 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..412b3e7 100644 --- a/tests/default.tests.js +++ b/tests/default.tests.js @@ -1,6 +1,10 @@ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; -import SimpleSchema from 'simpl-schema'; +import SimpleSchema from "meteor/aldeed:simple-schema"; +import { Meteor } from 'meteor/meteor'; +import { callMongoMethod } from './helper'; + +/* global it */ const defaultValuesSchema = new SimpleSchema({ bool1: { @@ -11,33 +15,70 @@ const defaultValuesSchema = new SimpleSchema({ const defaultValues = new Mongo.Collection('dv'); defaultValues.attachSchema(defaultValuesSchema); +global.defaultValues = defaultValues; export default function addDefaultValuesTests() { - it('defaultValues', function (done) { - let p; + if (Meteor.isServer) { + it('defaultValues', function (done) { + let p; + + // 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 + callMongoMethod(defaultValues, 'insert', [ + { + bool1: true + } + ]) + .then(async (testId2) => { + p = await callMongoMethod(defaultValues, 'findOne', [testId2]); + expect(p.bool1).toBe(true); - // Base case - defaultValues.insert({}, (error, testId1) => { - p = defaultValues.findOne(testId1); + 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 - defaultValues.insert({ - bool1: true - }, (err, testId2) => { - p = defaultValues.findOne(testId2); - expect(p.bool1).toBe(true); + const testId2 = await callMongoMethod(defaultValues, 'insert', [{ bool1: true }]); + p = await callMongoMethod(defaultValues, 'findOne', [testId2]); + expect(p.bool1).toBe(true); - defaultValues.update(testId1, { + await callMongoMethod(defaultValues, 'update', [ + testId1, + { $set: { bool1: true } - }, () => { - p = defaultValues.findOne(testId1); - expect(p.bool1).toBe(true); - done(); - }); - }); + } + ]); + p = await callMongoMethod(defaultValues, 'findOne', [testId1]); + expect(p.bool1).toBe(true); }); - }); -}; + } +} diff --git a/tests/defaultCleanOptions.tests.js b/tests/defaultCleanOptions.tests.js deleted file mode 100644 index 8cbac2a..0000000 --- a/tests/defaultCleanOptions.tests.js +++ /dev/null @@ -1,27 +0,0 @@ -import expect from 'expect'; -import Collection2 from 'meteor/aldeed:collection2'; - -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('allows setting cleanOptions', function () { - 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 diff --git a/tests/helper.js b/tests/helper.js new file mode 100644 index 0000000..7df3827 --- /dev/null +++ b/tests/helper.js @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; + +let ASYNC_FRIENDLY = false; + +if (Mongo.Collection.prototype.insertAsync) { + ASYNC_FRIENDLY = true; +} + +const getMethodNameByMeteorVersion = (methodName) => + ASYNC_FRIENDLY ? `${methodName}Async` : methodName; + +export function callMongoMethod(collection, method, args) { + const methodName = getMethodNameByMeteorVersion(method); + + return new Promise((resolve, reject) => { + if (ASYNC_FRIENDLY) { + 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 (ASYNC_FRIENDLY) { + resolve(collection.find(selector).fetchAsync()); + } else { + resolve(collection.find(selector).fetch()); + } + }); +} diff --git a/tests/multi.tests.js b/tests/multi.tests.js index 29e0032..5ffd5a1 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 SimpleSchema from "meteor/aldeed:simple-schema"; +import { Meteor } from 'meteor/meteor'; +import { callMeteorFetch, callMongoMethod } from './helper'; + +/* global describe, it, beforeEach */ const productSchema = new SimpleSchema({ _id: { @@ -9,16 +13,16 @@ 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.' } }); @@ -29,27 +33,27 @@ 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 } }); @@ -57,7 +61,7 @@ const extendedProductSchema = new SimpleSchema(productSchema); extendedProductSchema.extend({ barcode: { type: String, - defaultValue: "ABC123" + defaultValue: 'ABC123' } }); @@ -70,43 +74,70 @@ 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.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'}}); +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.attachSchema(productSchema, { + selector: { type: 'simple' } + }); + extendedProducts.attachSchema(productVariantSchema, { + selector: { type: 'variant' } + }); + extendedProducts.attachSchema(extendedProductSchema, { + selector: { type: 'simple' } + }); } 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'); // Attach two different schemas - c.attachSchema(new SimpleSchema({ - one: { type: String } - })); - c.attachSchema(new SimpleSchema({ - two: { type: String } - })); + c.attachSchema( + new SimpleSchema({ + one: { type: String } + }) + ); + c.attachSchema( + new SimpleSchema({ + two: { type: String } + }) + ); // Check the combined schema let combinedSchema = c.simpleSchema(); @@ -115,9 +146,11 @@ export default function addMultiTests() { 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 } - })); + 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); @@ -128,94 +161,116 @@ export default function addMultiTests() { 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); + 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({ - 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 productId3 = await callMongoMethod(products, 'insert', [ + { + title: 'Product three', + createdAt: new Date(), + type: 'variant' // other selector in doc + } + ]); + 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({ - title: 'Product one' - }, { selector: { type: 'simple' } }); + it('insert selects the correct schema', async function () { + const productId = await callMongoMethod(products, 'insert', [ + { + title: 'Product one' + }, + { selector: { type: 'simple' } } + ]); - const productVariantId = products.insert({ - title: 'Product variant one', - createdAt: new Date() - }, { selector: { type: 'variant' } }); + const productVariantId = await callMongoMethod(products, 'insert', [ + { + title: 'Product variant one', + createdAt: new Date() + }, + { 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(productVariant.price).toBe(5) + expect(productVariant.price).toBe(5); }); - it('inserts doc correctly with selector passed via doc and via