diff --git a/package/collection2/main.js b/package/collection2/main.js index fd8e968..1c418ac 100644 --- a/package/collection2/main.js +++ b/package/collection2/main.js @@ -7,6 +7,36 @@ import isEqual from 'lodash.isequal'; import isObject from 'lodash.isobject'; import { flattenSelector, isInsertType, isUpdateType, isUpsertType } from './lib'; +const C2 = {}; +C2._validator = null; + +C2.init = self => { + self._c2 = self._c2 || Object.create(null); + self._c2.schemas = self._c2.schemas || [null]; + return self; +}; + +C2.validator = () => { + const validator= C2._validator + if (!validator) { + throw new Error(`Cannot attach a schema if no validation library has been added`); + } + return validator; +} + +C2.schemas = (self) => { + if (!self._c2) { + C2.init(self); + } + return self._c2.schemas; +} + +Collection2.defineValidation = ({ freeze, ...validator }) => { + C2._validator = validator; + // TODO if freeze it should not be writable or deletable +} + +Object.assign(Collection2, { isInsertType, isUpsertType, isUpdateType }) /** * Mongo.Collection.prototype.attachSchema @@ -25,52 +55,58 @@ import { flattenSelector, isInsertType, isUpdateType, isUpsertType } from './lib * schema object passed to its constructor. */ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { - options = options || {}; - + options = options || Object.create(null); + + const self = this; + const validator = C2.validator(); + // Allow passing just the schema object - if (!SimpleSchema.isSimpleSchema(ss)) { - ss = new SimpleSchema(ss); + if (!validator.is(ss)) { + ss = validator.create(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]; - + C2.init(obj); + + const allSchemas = C2.schemas(obj); + 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); + const base = allSchemas[0]; + if (base) { + ss = validator.extend(base.schema, ss); } // Index of existing schema with identical selector - let schemaIndex; + let index; // 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; + for (index = allSchemas.length - 1; index > 0; index--) { + const current = allSchemas[index]; + if (current && isEqual(current.selector, options.selector)) break; } - if (schemaIndex <= 0) { + if (index <= 0) { // We didn't find the schema in our array - push it into the array - obj._c2._simpleSchemas.push({ + allSchemas.push({ schema: ss, selector: options.selector }); - } else { + } + 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 { + allSchemas[index].schema = ss; + } + else { // Extend existing selector schema with new selector schema. - obj._c2._simpleSchemas[schemaIndex].schema = extendSchema( - obj._c2._simpleSchemas[schemaIndex].schema, + allSchemas[index].schema = validator.extend( + allSchemas[index].schema, ss ); } @@ -79,23 +115,21 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { // Base Schema if (options.replace === true) { // Replace base schema and delete all other schemas - obj._c2._simpleSchemas = [ - { - schema: ss, - selector: options.selector - } - ]; + obj._c2.schemas = [{ + 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]; + if (!allSchemas[0]) { + allSchemas[0] = { schema: ss, selector: undefined }; + return allSchemas[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, + allSchemas.forEach((schema, i) => { + if (allSchemas[i]) { + allSchemas[i].schema = validator.extend( + allSchemas[i].schema, ss ); } @@ -104,20 +138,20 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { } } - attachTo(this); + attachTo(self); // Attach the schema to the underlying LocalCollection, too - if (this._collection instanceof LocalCollection) { - this._collection._c2 = this._collection._c2 || {}; - attachTo(this._collection); + if (self._collection instanceof LocalCollection) { + C2.init(self._collection); + attachTo(self._collection); } - defineDeny(this, options); - keepInsecure(this); + defineDeny(self, options); + keepInsecure(self); - Collection2.emit('schema.attached', this, ss, options); + Collection2.emit('schema.attached', self, ss, options); }; - [Mongo.Collection, LocalCollection].forEach((obj) => { +[Mongo.Collection, LocalCollection].forEach((obj) => { /** * simpleSchema * @description function detect the correct schema by given params. If it @@ -129,16 +163,18 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { * @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) { + obj.prototype.c2Schema = function (doc, options, query) { + const self = this; + if (!self._c2) return null; + if (self._c2._schema) return self._c2._schema; + + const allSchemas = C2.schemas(self); + + if (allSchemas && allSchemas.length > 0) { let schema, selector, target; // Position 0 reserved for base schema - for (let i = 1; i < schemas.length; i++) { - schema = schemas[i]; + for (let i = 1; i < allSchemas.length; i++) { + schema = allSchemas[i]; selector = Object.keys(schema.selector)[0]; // We will set this to undefined because in theory, you might want to select @@ -163,8 +199,8 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { return schema.schema; } } - if (schemas[0]) { - return schemas[0].schema; + if (allSchemas[0]) { + return allSchemas[0].schema; } else { throw new Error('No default schema'); } @@ -173,8 +209,8 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { return null; }; }); - - function _methodMutation(async, methodName) { + +function _methodMutation(async, methodName) { const _super = Meteor.isFibersDisabled ? Mongo.Collection.prototype[methodName] : Mongo.Collection.prototype[methodName.replace('Async', '')]; @@ -198,15 +234,15 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { userId = Meteor.userId(); } catch (err) {} - [args, validationContext] = doValidate( - this, - methodName, + [args, validationContext] = doValidate({ + collection: this, + type: methodName, args, - Meteor.isServer || this._connection === null, // getAutoValues + getAutoValues: Meteor.isServer || this._connection === null, // getAutoValues userId, - Meteor.isServer, // isFromTrustedCode + isFromTrustedCode: Meteor.isServer, // isFromTrustedCode async - ); + }); if (!args) { // doValidate already called the callback or threw the error, so we're done. @@ -238,7 +274,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { }; } - function _methodMutationAsync(methodName) { +function _methodMutationAsync(methodName) { const _super = Mongo.Collection.prototype[methodName]; Mongo.Collection.prototype[methodName] = async function (...args) { let options = isInsertType(methodName) ? args[1] : args[2]; @@ -256,15 +292,15 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { userId = Meteor.userId(); } catch (err) {} - [args, validationContext] = doValidate( - this, - methodName, + [args, validationContext] = doValidate({ + collection: this, + type: methodName, args, - Meteor.isServer || this._connection === null, // getAutoValues + getAutoValues: Meteor.isServer || this._connection === null, // getAutoValues userId, - Meteor.isServer, // isFromTrustedCode - true - ); + isFromTrustedCode: Meteor.isServer, // isFromTrustedCode + async: true + }); if (!args) { // doValidate already called the callback or threw the error, so we're done. @@ -290,20 +326,21 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { } // 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)); - } +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)); +} + +['insert', 'update'].forEach(_methodMutation.bind(this, false)); /* * Private */ - function doValidate(collection, type, args, getAutoValues, userId, isFromTrustedCode, async) { +function doValidate({ collection, type, args = [], getAutoValues, userId, isFromTrustedCode, async }) { let doc, callback, error, options, selector; if (!args.length) { @@ -340,8 +377,8 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { callback = options; options = {}; } - options = options || {}; - + options = options || Object.create(null); + const last = args.length - 1; const hasCallback = typeof args[last] === 'function'; @@ -351,14 +388,14 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { // we need to pass `doc` and `options` to `simpleSchema` method, that's why // schema declaration moved here - let schema = collection.simpleSchema(doc, options, selector); + let schema = collection.c2Schema(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; @@ -575,7 +612,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { } } - function getErrorObject(context, appendToMessage = '', code) { +function getErrorObject(context, appendToMessage = '', code) { let message; const invalidKeys = typeof context.validationErrors === 'function' @@ -609,7 +646,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { return error; } - function addUniqueError(context, errorMessage) { +function addUniqueError(context, errorMessage) { const name = errorMessage.split('c2_')[1].split(' ')[0]; const val = errorMessage.split('dup key:')[1].split('"')[1]; @@ -624,7 +661,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { ]); } - function parsingServerError(args, validationContext, addValidationErrorsPropName) { +function parsingServerError(args, validationContext, addValidationErrorsPropName) { const error = args[0]; // Handle our own validation errors if ( @@ -649,7 +686,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { } } - function wrapCallbackForParsingMongoValidationErrors(validationContext, cb) { +function wrapCallbackForParsingMongoValidationErrors(validationContext, cb) { return function wrappedCallbackForParsingMongoValidationErrors(...args) { const error = args[0]; if ( @@ -665,7 +702,7 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { }; } - function wrapCallbackForParsingServerErrors(validationContext, cb) { +function wrapCallbackForParsingServerErrors(validationContext, cb) { const addValidationErrorsPropName = typeof validationContext.addValidationErrors === 'function' ? 'addValidationErrors' @@ -676,9 +713,9 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { }; } - const alreadyInsecure = {}; +const alreadyInsecure = {}; - function keepInsecure(c) { +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]) { @@ -715,162 +752,121 @@ Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) { // 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 - } - }); +C2.alreadyDefined = {}; - 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, +function defineDeny(collection, options) { + const validator = C2.validator(); + if (C2.alreadyDefined[collection._name]) { + return false; // no definition added; + } + const isLocalCollection = collection._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 + const schema = collection.c2Schema(doc); + validator.clean({ doc, schema, userId, isLocalCollection, type: 'insert' }); + return false; + }, + update: function (userId, doc, fields, modifier) { + // Referenced modifier is cleaned in place + const schema = collection.c2Schema(doc); + validator.clean({ userId, doc, fields, modifier, schema, type: 'update' }); + return false; + }, + fetch: ['_id'], + transform: null + }; + + if (Meteor.isFibersDisabled) { + Object.assign(firstDeny, { + insertAsync: firstDeny.insert, + updateAsync: firstDeny.update + }); + } + + collection.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({ + collection, + type: 'insert', + args: [ + doc, + { + trimStrings: false, removeEmptyStrings: false, + filter: false, + autoConvert: false + }, + function (error) { + if (error) { + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); + } + } + ], + getAutoValues: false, // getAutoValues + userId, + isFromTrustedCode: 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({ + collection, + type: 'update', + args: [ + { _id: doc && doc._id }, + modifier, + { trimStrings: false, - extendAutoValueContext: { - isInsert: false, - isUpdate: true, - isUpsert: false, - userId, - isFromTrustedCode: false, - docId: doc && doc._id, - isLocalCollection + removeEmptyStrings: false, + filter: false, + autoConvert: false + }, + function (error) { + if (error) { + throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys)); } - }); - - 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; - } + } + ], + getAutoValues: false, // getAutoValues + userId, + isFromTrustedCode: false // isFromTrustedCode + }); + + return false; + }, + fetch: ['_id'], + ...(options.transform === true ? {} : { transform: null }) + }; + + if (Meteor.isFibersDisabled) { + Object.assign(secondDeny, { + insertAsync: secondDeny.insert, + updateAsync: secondDeny.update + }); } - - 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 + + collection.deny(secondDeny); + + // note that we've already done this collection so that we don't do it again + // if attachSchema is called again + C2.alreadyDefined[collection._name] = true; + return true; // new definition added +} diff --git a/tests/.meteor/versions b/tests/.meteor/versions index 16d345e..d2d1866 100644 --- a/tests/.meteor/versions +++ b/tests/.meteor/versions @@ -35,8 +35,8 @@ jquery@3.0.0 logging@1.3.3-beta300.0 meteor@2.0.0-beta300.0 meteor-base@1.5.2-beta300.0 -meteortesting:browser-tests@1.5.3 -meteortesting:mocha@2.1.0 +meteortesting:browser-tests@1.6.0-beta300.0 +meteortesting:mocha@3.1.0-beta300.0 meteortesting:mocha-core@8.3.1-beta300.0 minifier-css@2.0.0-beta300.0 minifier-js@3.0.0-beta300.0 diff --git a/tests/ajv.tests.js b/tests/ajv.tests.js new file mode 100644 index 0000000..7c4f4a6 --- /dev/null +++ b/tests/ajv.tests.js @@ -0,0 +1,44 @@ +/* eslint-env mocha */ +import Ajv from 'ajv' +import expect from 'expect' +import { callMongoMethod } from './helper' +import { ajvImpl } from './libraries' + +describe('using ajv', () => { + before(() => { + Collection2.defineValidation(ajvImpl()); + }) + + it('attach and get ajv for normal collection', function () { + ;['ajvMc1', null].forEach(name => { + const mc = new Mongo.Collection(name, Meteor.isClient ? { connection: null } : undefined); + + mc.attachSchema({ + type: "object", + properties: { foo: { type: "string" } }, + required: ["foo"], + additionalProperties: false, + }); + + expect(mc.c2Schema() instanceof Ajv).toBe(true); + }); + }); + it('handles prototype-less objects', async function () { + const prototypelessTest = new Mongo.Collection( + 'prototypelessTestAjv', + Meteor.isClient ? { connection: null } : undefined + ); + + prototypelessTest.attachSchema({ + type: "object", + properties: { foo: { type: "string" } }, + required: ["foo"], + additionalProperties: false, + }); + + const prototypelessObject = Object.create(null); + prototypelessObject.foo = 'bar'; + + await callMongoMethod(prototypelessTest, 'insert', [prototypelessObject]); + }); +}) \ No newline at end of file diff --git a/tests/autoValue.tests.js b/tests/autoValue.tests.js index 8a10950..c933c1f 100644 --- a/tests/autoValue.tests.js +++ b/tests/autoValue.tests.js @@ -1,41 +1,50 @@ -/* eslint-env mocha */ import 'meteor/aldeed:collection2/static'; import { Meteor } from 'meteor/meteor'; -import expect from 'expect'; import { Mongo } from 'meteor/mongo'; +import expect from 'expect'; import SimpleSchema from "meteor/aldeed:simple-schema"; import { callMongoMethod } from './helper'; +import { Collection2 } from 'meteor/aldeed:collection2' +import { simpleSchemaImpl } from './libraries' const collection = new Mongo.Collection('autoValueTestCollection'); 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; +const attach = () => { + [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() { + console.debug('get autovalues', Meteor.isClient) + if (Meteor.isClient) return; + return (this.value || 0) + 1; + } } - } - }) - ); -}); + }) + ); + }); +} if (Meteor.isClient) { describe('autoValue on client', function () { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()); + attach() + }) + 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) { @@ -79,6 +88,10 @@ if (Meteor.isClient) { if (Meteor.isServer) { describe('autoValue on server', function () { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + attach() + }) it('runs function once', async function () { const id = await callMongoMethod(collection, 'insert', [{}]); const doc = await callMongoMethod(collection, 'findOne', [id]); diff --git a/tests/books.tests.js b/tests/books.tests.js index 174997a..8faab30 100644 --- a/tests/books.tests.js +++ b/tests/books.tests.js @@ -4,6 +4,8 @@ import SimpleSchema from "meteor/aldeed:simple-schema"; import { Meteor } from 'meteor/meteor'; import { _ } from 'meteor/underscore'; import { callMeteorFetch, callMongoMethod } from './helper'; +import { Collection2 } from 'meteor/aldeed:collection2' +import { simpleSchemaImpl } from './libraries' /* global describe, it, beforeEach */ @@ -57,17 +59,20 @@ const booksSchema = new SimpleSchema({ }); 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 } + +describe('SimpleSchema books tests', () => { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + books.attachSchema(booksSchema); + upsertTest.attachSchema( + new SimpleSchema({ + _id: { type: String }, + foo: { type: Number } + }) + ); }) -); -export default function addBooksTests() { describe('insert', function () { beforeEach(async function () { for (const book of await callMeteorFetch(books, {})) { @@ -89,8 +94,8 @@ export default function addBooksTests() { 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(); + // The list of errors is available by calling books.c2Schema().namedContext().validationErrors() + const validationErrors = books.c2Schema().namedContext().validationErrors(); expect(validationErrors.length).toBe(1); const key = validationErrors[0] || {}; @@ -122,7 +127,7 @@ export default function addBooksTests() { }) .catch(async (error) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse') .validationErrors(); @@ -150,7 +155,7 @@ export default function addBooksTests() { } ]).then(async (newId) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse2') .validationErrors(); @@ -181,7 +186,7 @@ export default function addBooksTests() { }) .catch(async (error) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse3') .validationErrors(); @@ -214,7 +219,7 @@ export default function addBooksTests() { } ]).then(async (result) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse4') .validationErrors(); expect(result).toBe(1); @@ -249,8 +254,8 @@ export default function addBooksTests() { // 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(); + // by calling books.c2Schema().namedContext().validationErrors() + const validationErrors = books.c2Schema().namedContext().validationErrors(); expect(validationErrors.length).toBe(1); const key = validationErrors[0] || {}; @@ -284,7 +289,7 @@ export default function addBooksTests() { } let validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse2') .validationErrors(); @@ -316,7 +321,7 @@ export default function addBooksTests() { } let updatedBook; - validationErrors = books.simpleSchema().namedContext('validateFalse3').validationErrors(); + validationErrors = books.c2Schema().namedContext('validateFalse3').validationErrors(); // When validated: false on the server, validation should be skipped expect(!!error).toBe(false); @@ -350,7 +355,7 @@ export default function addBooksTests() { error = e; } - validationErrors = books.simpleSchema().namedContext('validateFalse4').validationErrors(); + validationErrors = books.c2Schema().namedContext('validateFalse4').validationErrors(); expect(!!error).toBe(false); expect(result).toBe(1); expect(validationErrors.length).toBe(0); @@ -380,34 +385,34 @@ export default function addBooksTests() { if (Meteor.isServer) { describe('upsert', function () { - function getCallback(done) { + function getCallback (done) { return (result) => { expect(result.numberAffected).toBe(1); - const validationErrors = books.simpleSchema().namedContext().validationErrors(); + const validationErrors = books.c2Schema().namedContext().validationErrors(); expect(validationErrors.length).toBe(0); done(); }; } - function getUpdateCallback(done) { + function getUpdateCallback (done) { return (result) => { expect(result).toBe(1); - const validationErrors = books.simpleSchema().namedContext().validationErrors(); + const validationErrors = books.c2Schema().namedContext().validationErrors(); expect(validationErrors.length).toBe(0); done(); }; } - function getErrorCallback(done) { + function getErrorCallback (done) { return (error) => { expect(!!error).toBe(true); // expect(!!result).toBe(false) - const validationErrors = books.simpleSchema().namedContext().validationErrors(); + const validationErrors = books.c2Schema().namedContext().validationErrors(); expect(validationErrors.length).toBe(1); done(); @@ -582,7 +587,7 @@ export default function addBooksTests() { ]) .then(async (result) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse') .validationErrors(); @@ -610,7 +615,7 @@ export default function addBooksTests() { newId = _newId; const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse2') .validationErrors(); @@ -639,7 +644,7 @@ export default function addBooksTests() { }) .then((result) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse3') .validationErrors(); @@ -670,7 +675,7 @@ export default function addBooksTests() { }) .then((result) => { const validationErrors = books - .simpleSchema() + .c2Schema() .namedContext('validateFalse4') .validationErrors(); expect(result).toBe(1); @@ -737,4 +742,4 @@ export default function addBooksTests() { expect(doc.foo).toBe(2); }); } -} +}); diff --git a/tests/clean.tests.js b/tests/clean.tests.js index c2d8f2c..b2696ad 100644 --- a/tests/clean.tests.js +++ b/tests/clean.tests.js @@ -1,10 +1,12 @@ +/* eslint-env mocha */ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from "meteor/aldeed:simple-schema"; import { Meteor } from 'meteor/meteor'; +import { Collection2 } from 'meteor/aldeed:collection2' import { callMongoMethod } from './helper'; +import { simpleSchemaImpl } from './libraries' -/* global describe it */ let collection; @@ -14,7 +16,10 @@ if (Meteor.isClient) { collection = new Mongo.Collection('cleanTests'); } -describe('clean options', function () { +describe('SimpleSchema clean options', function () { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + }) describe('filter', function () { it('keeps default schema clean options', function (done) { const schema = new SimpleSchema( diff --git a/tests/collection2.tests.js b/tests/collection2.tests.js index 5767697..8e0946b 100644 --- a/tests/collection2.tests.js +++ b/tests/collection2.tests.js @@ -1,16 +1,17 @@ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; 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'; +import { Collection2 } from 'meteor/aldeed:collection2' +import { simpleSchemaImpl } from './libraries' /* global describe, it */ describe('collection2', function () { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + }) it('attach and get simpleSchema for normal collection', function () { const mc = new Mongo.Collection('mc', Meteor.isClient ? { connection: null } : undefined); @@ -20,7 +21,7 @@ describe('collection2', function () { }) ); - expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true); + expect(mc.c2Schema() instanceof SimpleSchema).toBe(true); }); it('attach and get simpleSchema for local collection', function () { @@ -32,7 +33,7 @@ describe('collection2', function () { }) ); - expect(mc.simpleSchema() instanceof SimpleSchema).toBe(true); + expect(mc.c2Schema() instanceof SimpleSchema).toBe(true); }); it('handles prototype-less objects', async function () { @@ -686,8 +687,5 @@ describe('collection2', function () { } }); - addBooksTests(); - addContextTests(); - addDefaultValuesTests(); - addMultiTests(); + }); diff --git a/tests/context.tests.js b/tests/context.tests.js index 6576feb..8c2dcb7 100644 --- a/tests/context.tests.js +++ b/tests/context.tests.js @@ -1,10 +1,12 @@ +/* eslint-env mocha */ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from "meteor/aldeed:simple-schema"; import { Meteor } from 'meteor/meteor'; import { callMongoMethod } from './helper'; +import { Collection2 } from 'meteor/aldeed:collection2' +import { simpleSchemaImpl } from './libraries' -/* global it */ const contextCheckSchema = new SimpleSchema({ foo: { @@ -54,9 +56,14 @@ const contextCheckSchema = new SimpleSchema({ }); const contextCheck = new Mongo.Collection('contextCheck'); -contextCheck.attachSchema(contextCheckSchema); -export default function addContextTests() { + +describe('context tests', () => { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + contextCheck.attachSchema(contextCheckSchema); + }) + it('AutoValue Context', async function () { const testId = await callMongoMethod(contextCheck, 'insert', [{}]); @@ -101,4 +108,4 @@ export default function addContextTests() { 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 412b3e7..3caec70 100644 --- a/tests/default.tests.js +++ b/tests/default.tests.js @@ -1,10 +1,11 @@ +/* eslint-env mocha */ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from "meteor/aldeed:simple-schema"; import { Meteor } from 'meteor/meteor'; import { callMongoMethod } from './helper'; - -/* global it */ +import { simpleSchemaImpl } from './libraries' +import { Collection2 } from 'meteor/aldeed:collection2' const defaultValuesSchema = new SimpleSchema({ bool1: { @@ -14,10 +15,14 @@ const defaultValuesSchema = new SimpleSchema({ }); const defaultValues = new Mongo.Collection('dv'); -defaultValues.attachSchema(defaultValuesSchema); global.defaultValues = defaultValues; -export default function addDefaultValuesTests() { +describe('defaults tests', () => { + before(() => { + Collection2.defineValidation(simpleSchemaImpl()) + defaultValues.attachSchema(defaultValuesSchema); + }) + if (Meteor.isServer) { it('defaultValues', function (done) { let p; @@ -81,4 +86,4 @@ export default function addDefaultValuesTests() { expect(p.bool1).toBe(true); }); } -} +}); diff --git a/tests/libraries.js b/tests/libraries.js new file mode 100644 index 0000000..b59cc1c --- /dev/null +++ b/tests/libraries.js @@ -0,0 +1,65 @@ +import Ajv from 'ajv' +import SimpleSchema from 'meteor/aldeed:simple-schema' +import { Collection2 } from 'meteor/aldeed:collection2' +export const ajvImpl = () => ({ + name: 'ajv', + is: schema => schema instanceof Ajv, + create: schema => { + const instance = new Ajv() + instance.definition = schema + return instance + }, + extend: (s1, s2) => { + // not impl + return s2 + }, + clean: ({ doc, modifier, schema, userId, isLocalCollection, type }) => { + // not impl + }, + validate: () => {}, + freeze: false +}) + +export const simpleSchemaImpl = () => ({ + name: 'SimpleSchema', + is: schema => SimpleSchema.isSimpleSchema(schema), + create: schema => new SimpleSchema(schema), + extend: (s1, s2) => { + if (s2.version >= 2) { + const ss = new SimpleSchema(s1); + ss.extend(s2); + return ss; + } else { + return new SimpleSchema([s1, s2]); + } + }, + clean: ({ doc, modifier, schema, userId, isLocalCollection, type }) => { + const isModifier = !Collection2.isInsertType(type); + const target = isModifier ? modifier : doc; + schema.clean(target, { + mutate: true, + isModifier, + // 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: Collection2.isInsertType(type), + isUpdate: Collection2.isUpdateType(type), + isUpsert: Collection2.isUpdateType(type), + userId, + isFromTrustedCode: false, + docId: doc?._id, + isLocalCollection + } + }) + }, + validate: ({}) => { + + }, + getErrors: () => { + + }, + freeze: false +}) \ No newline at end of file diff --git a/tests/main.tests.js b/tests/main.tests.js new file mode 100644 index 0000000..882c480 --- /dev/null +++ b/tests/main.tests.js @@ -0,0 +1,9 @@ +import 'meteor/aldeed:collection2/static'; +import './autoValue.tests' +import './clean.tests' +import './collection2.tests' +import './multi.tests.js'; +import './books.tests.js'; +import './context.tests.js'; +import './default.tests.js'; +import './ajv.tests' diff --git a/tests/multi.tests.js b/tests/multi.tests.js index 5ffd5a1..ecb8cdd 100644 --- a/tests/multi.tests.js +++ b/tests/multi.tests.js @@ -1,11 +1,10 @@ +/* eslint-env mocha */ import expect from 'expect'; import { Mongo } from 'meteor/mongo'; 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: { type: String, @@ -69,495 +68,496 @@ extendedProductSchema.extend({ // Need to define the client one on both client and server let products = new Mongo.Collection('TestProductsClient'); -products.attachSchema(productSchema, { selector: { type: 'simple' } }); -products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }); -if (Meteor.isServer) { - products = new Mongo.Collection('TestProductsServer'); - products.attachSchema(productSchema, { selector: { type: 'simple' } }); - products.attachSchema(productVariantSchema, { - selector: { type: 'variant' } - }); -} +let extendedProducts = new Mongo.Collection('ExtendedProductsClient'); + +describe('multiple top-level schemas', function () { + before(() => { + products.attachSchema(productSchema, { selector: { type: 'simple' } }); + products.attachSchema(productVariantSchema, { selector: { type: 'variant' } }); + if (Meteor.isServer) { + products = new Mongo.Collection('TestProductsServer'); + products.attachSchema(productSchema, { selector: { type: 'simple' } }); + products.attachSchema(productVariantSchema, { + selector: { type: 'variant' } + }); + } -/* Extended Products */ + /* 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' } -}); -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' } - }); -} - -export default function addMultiTests() { - describe('multiple top-level schemas', function () { - 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); - } - */ + 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' } + }); + } + }) + beforeEach(async function () { + for (const doc of await callMeteorFetch(products, {})) { + await callMongoMethod(products, 'remove', [doc._id]); + } - it('works', function () { - const c = new Mongo.Collection('multiSchema'); + for (const doc of await callMeteorFetch(extendedProducts, {})) { + await callMongoMethod(products, 'remove', [doc._id]); + } - // Attach two different schemas - c.attachSchema( - new SimpleSchema({ - one: { type: String } - }) - ); - c.attachSchema( - new SimpleSchema({ - two: { type: String } - }) - ); + /* + for await (const doc of products.find({})) { + await products.removeAsync(doc._id); + } - // Check the combined schema - let combinedSchema = c.simpleSchema(); - expect(combinedSchema._schemaKeys.includes('one')).toBe(true); - expect(combinedSchema._schemaKeys.includes('two')).toBe(true); - expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(String)); - - // Attach a third schema and make sure that it extends/overwrites the others - c.attachSchema( - new SimpleSchema({ - two: { type: SimpleSchema.Integer } - }) - ); - combinedSchema = c.simpleSchema(); - expect(combinedSchema._schemaKeys.includes('one')).toBe(true); - expect(combinedSchema._schemaKeys.includes('two')).toBe(true); - expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(SimpleSchema.Integer)); - - // Ensure that we've only attached two deny functions - expect(c._validators.insert.deny.length).toBe(2); - expect(c._validators.update.deny.length).toBe(2); - }); + for await (const doc of extendedProducts.find({})) { + await products.removeAsync(doc._id); + } + */ + }); - 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 - } - ]); + 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 } + }) + ); + + // Check the combined schema + let combinedSchema = c.c2Schema(); + expect(combinedSchema._schemaKeys.includes('one')).toBe(true); + expect(combinedSchema._schemaKeys.includes('two')).toBe(true); + expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(String)); + + // Attach a third schema and make sure that it extends/overwrites the others + c.attachSchema( + new SimpleSchema({ + two: { type: SimpleSchema.Integer } + }) + ); + combinedSchema = c.c2Schema(); + expect(combinedSchema._schemaKeys.includes('one')).toBe(true); + expect(combinedSchema._schemaKeys.includes('two')).toBe(true); + expect(combinedSchema.schema('two').type).toEqual(SimpleSchema.oneOf(SimpleSchema.Integer)); + + // Ensure that we've only attached two deny functions + expect(c._validators.insert.deny.length).toBe(2); + expect(c._validators.update.deny.length).toBe(2); + }); - const product = await callMongoMethod(products, 'findOne', [productId]); - expect(product.description).toBe('This is a simple product.'); - expect(product.price).toBe(undefined); + if (Meteor.isServer) { + it('inserts doc correctly with selector passed via doc', async function () { + const productId = await callMongoMethod(products, 'insert', [ + { + title: 'Product one', + type: 'simple' // selector in doc + } + ]); + + const product = await callMongoMethod(products, 'findOne', [productId]); + expect(product.description).toBe('This is a simple product.'); + expect(product.price).toBe(undefined); + + const productId3 = 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); + }); - 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); - }); + // 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 .c2Schema() + // + // Also synchronous only works on server + it('insert selects the correct schema', async function () { + const productId = await callMongoMethod(products, 'insert', [ + { + title: 'Product one' + }, + { selector: { type: 'simple' } } + ]); + + const productVariantId = await callMongoMethod(products, 'insert', [ + { + title: 'Product variant one', + createdAt: new Date() + }, + { selector: { type: 'variant' } } + ]); + + 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); + }); - // 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', async function () { - const productId = await callMongoMethod(products, 'insert', [ - { - title: 'Product one' - }, - { selector: { type: 'simple' } } - ]); - - const productVariantId = await callMongoMethod(products, 'insert', [ - { - title: 'Product variant one', - createdAt: new Date() - }, - { selector: { type: 'variant' } } - ]); - - 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); - }); + it('inserts doc correctly with selector passed via doc and via