├── tests ├── .meteor │ ├── .gitignore │ ├── release │ ├── platforms │ ├── .id │ ├── .finished-upgraders │ ├── packages │ └── versions ├── .gitignore ├── .coverage.json └── package.json ├── .prettierignore ├── .meteorignore ├── lib ├── utility │ ├── isPromiseLike.js │ ├── appendAffectedKey.js │ ├── looksLikeModifier.js │ ├── dateToDateString.js │ ├── getParentOfKey.js │ ├── isEmptyObject.js │ ├── getLastPartOfKey.tests.js │ ├── getLastPartOfKey.js │ ├── getKeysWithValueInObj.js │ ├── forEachKeyAncestor.js │ ├── index.js │ ├── isObjectWeShouldTraverse.js │ └── merge.js ├── testHelpers │ ├── asyncTimeout.js │ ├── validate.js │ ├── expectValid.js │ ├── expectErrorLength.js │ ├── optionalCustomSchema.js │ ├── expectErrorOfTypeLength.js │ ├── expectRequiredErrorLength.js │ ├── requiredCustomSchema.js │ ├── Address.js │ ├── friendsSchema.js │ ├── requiredSchema.js │ └── testSchema.js ├── main.js ├── validation │ ├── allowedValuesValidator.js │ ├── typeValidator │ │ ├── doArrayChecks.js │ │ ├── doDateChecks.js │ │ ├── doStringChecks.js │ │ ├── doNumberChecks.js │ │ └── index.js │ └── requiredValidator.js ├── SimpleSchema_minCount.tests.js ├── SimpleSchema_rules.tests.js ├── SimpleSchemaGroup.js ├── SimpleSchema_omit.tests.js ├── expandShorthand.tests.js ├── SimpleSchema_definition.tests.js ├── SimpleSchema_namedContext.tests.js ├── main.tests.js ├── humanize.tests.js ├── humanize.js ├── clean │ ├── convertToProperType.tests.js │ ├── setAutoValues.tests.js │ ├── convertToProperType.js │ ├── setAutoValues.js │ ├── AutoValueRunner.js │ └── getPositionsForAutoValue.js ├── SimpleSchema_pick.tests.js ├── SimpleSchema_blackbox.tests.js ├── expandShorthand.js ├── SimpleSchema_labels.tests.js ├── defaultMessages.js ├── reactivity.tests.js ├── SimpleSchema_getQuickTypeForKey.tests.js ├── SimpleSchema_getObjectSchema.tests.js ├── SimpleSchema_autoValueFunctions.tests.js ├── SimpleSchema_oneOf.tests.js ├── regExp.js ├── SimpleSchema_custom.tests.js ├── ValidationContext.js ├── clean.js ├── SimpleSchema_extend.tests.js ├── SimpleSchema_min.tests.js ├── SimpleSchema_messages.tests.js ├── SimpleSchema_max.tests.js ├── SimpleSchema_allowedValues.tests.js └── SimpleSchema_regEx.tests.js ├── .markdownlint.json ├── .coverage.json ├── .versions ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── new-issue-comment.yml │ └── lint-test-publish.yml ├── LICENSE ├── .gitignore ├── package.js ├── CONTRIBUTING.md └── CHANGELOG.md /tests/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /tests/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@3.0.2 2 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .npm 2 | node_modules 3 | tests -------------------------------------------------------------------------------- /tests/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteorignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | *.tests.js 3 | .github 4 | .prettierignore 5 | .markdownlint.json 6 | .coverage.json -------------------------------------------------------------------------------- /lib/utility/isPromiseLike.js: -------------------------------------------------------------------------------- 1 | export const isPromiseLike = obj => obj && typeof obj.then === 'function' 2 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-emphasis-as-header": false, 4 | "no-inline-html": false 5 | } 6 | -------------------------------------------------------------------------------- /.coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/packages/simple-schema/**/*", 4 | "**/packages/*aldeed_simple_schema.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/testHelpers/asyncTimeout.js: -------------------------------------------------------------------------------- 1 | export const asyncTimeout = ms => new Promise(resolve => { 2 | setTimeout(() => resolve(), ms) 3 | }) 4 | -------------------------------------------------------------------------------- /tests/.coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/packages/simple-schema/**/*", 4 | "**/packages/*aldeed_simple_schema.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/testHelpers/validate.js: -------------------------------------------------------------------------------- 1 | export default function validate(ss, doc, options) { 2 | const context = ss.newContext(); 3 | context.validate(doc, options); 4 | return context; 5 | } 6 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema, ValidationContext } from './SimpleSchema'; 2 | import './clean'; 3 | 4 | SimpleSchema.ValidationContext = ValidationContext; 5 | 6 | export default SimpleSchema; 7 | -------------------------------------------------------------------------------- /lib/utility/appendAffectedKey.js: -------------------------------------------------------------------------------- 1 | export default function appendAffectedKey(affectedKey, key) { 2 | if (key === '$each') return affectedKey; 3 | return affectedKey ? `${affectedKey}.${key}` : key; 4 | } 5 | -------------------------------------------------------------------------------- /lib/testHelpers/expectValid.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import validate from './validate'; 3 | 4 | export default function expectValid(...args) { 5 | expect(validate(...args).isValid()).to.equal(true); 6 | } 7 | -------------------------------------------------------------------------------- /lib/testHelpers/expectErrorLength.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import validate from './validate'; 3 | 4 | export default function expectErrorLength(...args) { 5 | return expect(validate(...args).validationErrors().length); 6 | } 7 | -------------------------------------------------------------------------------- /lib/utility/looksLikeModifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if any of the keys of obj start with a $ 3 | */ 4 | export default function looksLikeModifier(obj) { 5 | return !!Object.keys(obj || {}).find((key) => key.substring(0, 1) === '$'); 6 | } 7 | -------------------------------------------------------------------------------- /lib/testHelpers/optionalCustomSchema.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | const optionalCustomSchema = new SimpleSchema({ 4 | foo: { 5 | type: String, 6 | optional: true, 7 | custom: () => 'custom', 8 | }, 9 | }); 10 | 11 | export default optionalCustomSchema; 12 | -------------------------------------------------------------------------------- /tests/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | qkddk0wcxczh.ptxyf1ib4i9f 8 | -------------------------------------------------------------------------------- /lib/utility/dateToDateString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a Date instance, returns a date string of the format YYYY-MM-DD 3 | */ 4 | export default function dateToDateString(date) { 5 | let m = (date.getUTCMonth() + 1); 6 | if (m < 10) m = `0${m}`; 7 | let d = date.getUTCDate(); 8 | if (d < 10) d = `0${d}`; 9 | return `${date.getUTCFullYear()}-${m}-${d}`; 10 | } 11 | -------------------------------------------------------------------------------- /lib/testHelpers/expectErrorOfTypeLength.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import validate from './validate'; 3 | 4 | export default function expectErrorOfTypeLength(type, ...args) { 5 | const errors = validate(...args).validationErrors(); 6 | 7 | let errorCount = 0; 8 | errors.forEach((error) => { 9 | if (error.type === type) errorCount++; 10 | }); 11 | return expect(errorCount); 12 | } 13 | -------------------------------------------------------------------------------- /lib/utility/getParentOfKey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the parent of a key. For example, returns 'a.b' when passed 'a.b.c'. 3 | * If no parent, returns an empty string. If withEndDot is true, the return 4 | * value will have a dot appended when it isn't an empty string. 5 | */ 6 | export default function getParentOfKey(key, withEndDot) { 7 | const lastDot = key.lastIndexOf('.'); 8 | return lastDot === -1 ? '' : key.slice(0, lastDot + Number(!!withEndDot)); 9 | } 10 | -------------------------------------------------------------------------------- /lib/testHelpers/expectRequiredErrorLength.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import validate from './validate'; 3 | import { SimpleSchema } from '../SimpleSchema'; 4 | 5 | export default function expectRequiredErrorLength(...args) { 6 | const errors = validate(...args).validationErrors(); 7 | 8 | let requiredErrorCount = 0; 9 | errors.forEach((error) => { 10 | if (error.type === SimpleSchema.ErrorTypes.REQUIRED) requiredErrorCount++; 11 | }); 12 | return expect(requiredErrorCount); 13 | } 14 | -------------------------------------------------------------------------------- /lib/utility/isEmptyObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Determines whether the object has any "own" properties 3 | * @param {Object} obj Object to test 4 | * @return {Boolean} True if it has no "own" properties 5 | */ 6 | export default function isEmptyObject(obj) { 7 | /* eslint-disable no-restricted-syntax */ 8 | for (const key in obj) { 9 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 10 | return false; 11 | } 12 | } 13 | /* eslint-enable no-restricted-syntax */ 14 | 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /lib/utility/getLastPartOfKey.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import getLastPartOfKey from './getLastPartOfKey'; 5 | 6 | describe('getLastPartOfKey', function () { 7 | it('returns the correct string for a non-array key', function () { 8 | expect(getLastPartOfKey('a.b.c', 'a')).to.equal('b.c'); 9 | }); 10 | 11 | it('returns the correct string for an array key', function () { 12 | expect(getLastPartOfKey('a.b.$.c', 'a.b')).to.equal('c'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/utility/getLastPartOfKey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the ending of key, after stripping out the beginning 3 | * ancestorKey and any array placeholders 4 | * 5 | * getLastPartOfKey('a.b.c', 'a') returns 'b.c' 6 | * getLastPartOfKey('a.b.$.c', 'a.b') returns 'c' 7 | */ 8 | export default function getLastPartOfKey(key, ancestorKey) { 9 | let lastPart = ''; 10 | const startString = `${ancestorKey}.`; 11 | if (key.indexOf(startString) === 0) { 12 | lastPart = key.replace(startString, ''); 13 | if (lastPart.startsWith('$.')) lastPart = lastPart.slice(2); 14 | } 15 | return lastPart; 16 | } 17 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | aldeed:simple-schema@2.0.0 2 | babel-compiler@7.11.0 3 | babel-runtime@1.5.2 4 | core-runtime@1.0.0 5 | dynamic-import@0.7.4 6 | ecmascript@0.16.9 7 | ecmascript-runtime@0.8.2 8 | ecmascript-runtime-client@0.12.2 9 | ecmascript-runtime-server@0.11.1 10 | fetch@0.1.5 11 | http@1.4.4 12 | inter-process-messaging@0.1.2 13 | local-test:aldeed:simple-schema@2.0.0 14 | meteor@2.0.1 15 | meteortesting:browser-tests@1.4.2 16 | meteortesting:mocha@2.1.0 17 | meteortesting:mocha-core@8.0.1 18 | modern-browsers@0.1.11 19 | modules@0.20.1 20 | modules-runtime@0.13.2 21 | promise@1.0.0 22 | react-fast-refresh@0.2.9 23 | tracker@1.3.4 24 | url@1.3.3 25 | -------------------------------------------------------------------------------- /lib/validation/allowedValuesValidator.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | export default function allowedValuesValidator() { 4 | if (!this.valueShouldBeChecked) return; 5 | 6 | const { allowedValues } = this.definition; 7 | if (!allowedValues) return; 8 | 9 | let isAllowed; 10 | // set defined in scope and allowedValues is its instance 11 | if (typeof Set === 'function' && allowedValues instanceof Set) { 12 | isAllowed = allowedValues.has(this.value); 13 | } else { 14 | isAllowed = allowedValues.includes(this.value); 15 | } 16 | 17 | return isAllowed ? true : SimpleSchema.ErrorTypes.VALUE_NOT_ALLOWED; 18 | } 19 | -------------------------------------------------------------------------------- /lib/utility/getKeysWithValueInObj.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an array of keys that are in obj, have a value 3 | * other than null or undefined, and start with matchKey 4 | * plus a dot. 5 | */ 6 | export default function getKeysWithValueInObj(obj, matchKey) { 7 | const keysWithValue = []; 8 | 9 | const keyAdjust = (k) => k.slice(0, matchKey.length + 1); 10 | const matchKeyPlusDot = `${matchKey}.`; 11 | 12 | Object.keys(obj || {}).forEach((key) => { 13 | const val = obj[key]; 14 | if (val === undefined || val === null) return; 15 | if (keyAdjust(key) === matchKeyPlusDot) { 16 | keysWithValue.push(key); 17 | } 18 | }); 19 | 20 | return keysWithValue; 21 | } 22 | -------------------------------------------------------------------------------- /lib/utility/forEachKeyAncestor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run loopFunc for each ancestor key in a dot-delimited key. For example, 3 | * if key is "a.b.c", loopFunc will be called first with ('a.b', 'c') and then with ('a', 'b.c') 4 | */ 5 | export default function forEachKeyAncestor(key, loopFunc) { 6 | let lastDot; 7 | 8 | // Iterate the dot-syntax hierarchy 9 | let ancestor = key; 10 | do { 11 | lastDot = ancestor.lastIndexOf('.'); 12 | if (lastDot !== -1) { 13 | ancestor = ancestor.slice(0, lastDot); 14 | const remainder = key.slice(ancestor.length + 1); 15 | loopFunc(ancestor, remainder); // Remove last path component 16 | } 17 | } while (lastDot !== -1); 18 | } 19 | -------------------------------------------------------------------------------- /tests/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /lib/utility/index.js: -------------------------------------------------------------------------------- 1 | export { default as appendAffectedKey } from './appendAffectedKey'; 2 | export { default as dateToDateString } from './dateToDateString'; 3 | export { default as forEachKeyAncestor } from './forEachKeyAncestor'; 4 | export { default as getKeysWithValueInObj } from './getKeysWithValueInObj'; 5 | export { default as getLastPartOfKey } from './getLastPartOfKey'; 6 | export { default as getParentOfKey } from './getParentOfKey'; 7 | export { default as isEmptyObject } from './isEmptyObject'; 8 | export { default as isObjectWeShouldTraverse } from './isObjectWeShouldTraverse'; 9 | export { default as looksLikeModifier } from './looksLikeModifier'; 10 | export { default as merge } from './merge'; 11 | -------------------------------------------------------------------------------- /lib/testHelpers/requiredCustomSchema.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | const requiredCustomSchema = new SimpleSchema({ 4 | a: { 5 | type: Array, 6 | custom() { 7 | // Just adding custom to trigger extra validation 8 | }, 9 | }, 10 | 'a.$': { 11 | type: Object, 12 | custom() { 13 | // Just adding custom to trigger extra validation 14 | }, 15 | }, 16 | b: { 17 | type: Array, 18 | custom() { 19 | // Just adding custom to trigger extra validation 20 | }, 21 | }, 22 | 'b.$': { 23 | type: Object, 24 | custom() { 25 | // Just adding custom to trigger extra validation 26 | }, 27 | }, 28 | }); 29 | 30 | export default requiredCustomSchema; 31 | -------------------------------------------------------------------------------- /lib/testHelpers/Address.js: -------------------------------------------------------------------------------- 1 | class Address { 2 | constructor(city, state) { 3 | this.city = city; 4 | this.state = state; 5 | } 6 | 7 | toString() { 8 | return `${this.city}, ${this.state}`; 9 | } 10 | 11 | clone() { 12 | return new Address(this.city, this.state); 13 | } 14 | 15 | equals(other) { 16 | if (!(other instanceof Address)) { 17 | return false; 18 | } 19 | return JSON.stringify(this) === JSON.stringify(other); 20 | } 21 | 22 | typeName() { // eslint-disable-line class-methods-use-this 23 | return 'Address'; 24 | } 25 | 26 | toJSONValue() { 27 | return { 28 | city: this.city, 29 | state: this.state, 30 | }; 31 | } 32 | } 33 | 34 | export default Address; 35 | -------------------------------------------------------------------------------- /lib/validation/typeValidator/doArrayChecks.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../../SimpleSchema'; 2 | 3 | export default function doArrayChecks(def, keyValue) { 4 | // Is it an array? 5 | if (!Array.isArray(keyValue)) { 6 | return { type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, dataType: 'Array' }; 7 | } 8 | 9 | // Are there fewer than the minimum number of items in the array? 10 | if (def.minCount !== null && keyValue.length < def.minCount) { 11 | return { type: SimpleSchema.ErrorTypes.MIN_COUNT, minCount: def.minCount }; 12 | } 13 | 14 | // Are there more than the maximum number of items in the array? 15 | if (def.maxCount !== null && keyValue.length > def.maxCount) { 16 | return { type: SimpleSchema.ErrorTypes.MAX_COUNT, maxCount: def.maxCount }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [storytellercz, jankapunkt] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /lib/validation/typeValidator/doDateChecks.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../../SimpleSchema'; 2 | import { dateToDateString } from '../../utility'; 3 | 4 | export default function doDateChecks(def, keyValue) { 5 | // Is it an invalid date? 6 | if (isNaN(keyValue.getTime())) return { type: SimpleSchema.ErrorTypes.BAD_DATE }; 7 | 8 | // Is it earlier than the minimum date? 9 | if (def.min && typeof def.min.getTime === 'function' && def.min.getTime() > keyValue.getTime()) { 10 | return { type: SimpleSchema.ErrorTypes.MIN_DATE, min: dateToDateString(def.min) }; 11 | } 12 | 13 | // Is it later than the maximum date? 14 | if (def.max && typeof def.max.getTime === 'function' && def.max.getTime() < keyValue.getTime()) { 15 | return { type: SimpleSchema.ErrorTypes.MAX_DATE, max: dateToDateString(def.max) }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /lib/SimpleSchema_minCount.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import friendsSchema from './testHelpers/friendsSchema'; 4 | import expectErrorLength from './testHelpers/expectErrorLength'; 5 | 6 | describe('SimpleSchema - minCount', function () { 7 | it('ensures array count is at least the minimum', function () { 8 | expectErrorLength(friendsSchema, { 9 | friends: [], 10 | enemies: [], 11 | }).to.deep.equal(1); 12 | 13 | expectErrorLength(friendsSchema, { 14 | $set: { 15 | friends: [], 16 | }, 17 | }, { modifier: true }).to.deep.equal(1); 18 | 19 | expectErrorLength(friendsSchema, { 20 | $setOnInsert: { 21 | friends: [], 22 | enemies: [], 23 | }, 24 | }, { modifier: true, upsert: true }).to.deep.equal(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/SimpleSchema_rules.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema - Rules', function () { 7 | it('Rules should be passed the object being validated', function () { 8 | const validationContext = new SimpleSchema({ 9 | foo: { 10 | type: Number, 11 | }, 12 | bar: { 13 | type: Number, 14 | max() { 15 | return this.obj.foo; 16 | }, 17 | }, 18 | }).newContext(); 19 | 20 | validationContext.validate({ foo: 5, bar: 10 }); 21 | expect(validationContext.validationErrors().length).to.equal(1); 22 | validationContext.validate({ foo: 10, bar: 5 }); 23 | expect(validationContext.validationErrors().length).to.equal(0); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Versions (please complete the following information):** 31 | - Meteor version: [e.g. 1.8.2] 32 | - Browser: [e.g. Firefox, Chrome, Safari] 33 | - Version: [e.g. 1.0.0] 34 | 35 | **Additional context** 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /lib/utility/isObjectWeShouldTraverse.js: -------------------------------------------------------------------------------- 1 | export default function isObjectWeShouldTraverse(val) { 2 | // Some of these types don't exist in old browsers so we'll catch and return false in those cases 3 | try { 4 | if (val !== Object(val)) return false; 5 | // There are some object types that we know we shouldn't traverse because 6 | // they will often result in overflows and it makes no sense to validate them. 7 | if (val instanceof Date) return false; 8 | if (val instanceof Int8Array) return false; 9 | if (val instanceof Uint8Array) return false; 10 | if (val instanceof Uint8ClampedArray) return false; 11 | if (val instanceof Int16Array) return false; 12 | if (val instanceof Uint16Array) return false; 13 | if (val instanceof Int32Array) return false; 14 | if (val instanceof Uint32Array) return false; 15 | if (val instanceof Float32Array) return false; 16 | if (val instanceof Float64Array) return false; 17 | } catch (e) { 18 | return false; 19 | } 20 | 21 | return true; 22 | } 23 | -------------------------------------------------------------------------------- /lib/SimpleSchemaGroup.js: -------------------------------------------------------------------------------- 1 | import MongoObject from 'mongo-object'; 2 | 3 | class SimpleSchemaGroup { 4 | constructor(...definitions) { 5 | this.definitions = definitions.map((definition) => { 6 | if (MongoObject.isBasicObject(definition)) { 7 | return { ...definition }; 8 | } 9 | 10 | if (definition instanceof RegExp) { 11 | return { 12 | type: String, 13 | regEx: definition, 14 | }; 15 | } 16 | 17 | return { type: definition }; 18 | }); 19 | } 20 | 21 | get singleType() { 22 | return this.definitions[0].type; 23 | } 24 | 25 | clone() { 26 | return new SimpleSchemaGroup(...this.definitions); 27 | } 28 | 29 | extend(otherGroup) { 30 | // We extend based on index being the same. No better way I can think of at the moment. 31 | this.definitions = this.definitions.map((def, index) => { 32 | const otherDef = otherGroup.definitions[index]; 33 | if (!otherDef) return def; 34 | return { ...def, ...otherDef }; 35 | }); 36 | } 37 | } 38 | 39 | export default SimpleSchemaGroup; 40 | -------------------------------------------------------------------------------- /lib/SimpleSchema_omit.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | it('omit', function () { 8 | const schema = new SimpleSchema({ 9 | foo: { type: Object }, 10 | 'foo.bar': { type: String }, 11 | fooArray: { type: Array }, 12 | 'fooArray.$': { type: Object }, 13 | 'fooArray.$.bar': { type: String }, 14 | }); 15 | 16 | let newSchema = schema.omit('foo'); 17 | expect(Object.keys(newSchema.schema())).to.deep.equal(['fooArray', 'fooArray.$', 'fooArray.$.bar']); 18 | 19 | newSchema = schema.omit('fooArray'); 20 | expect(Object.keys(newSchema.schema())).to.deep.equal(['foo', 'foo.bar']); 21 | 22 | newSchema = schema.omit('foo', 'fooArray'); 23 | expect(Object.keys(newSchema.schema())).to.deep.equal([]); 24 | 25 | newSchema = schema.omit('blah'); 26 | expect(Object.keys(newSchema.schema())).to.deep.equal(['foo', 'foo.bar', 'fooArray', 'fooArray.$', 'fooArray.$.bar']); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/expandShorthand.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import expandShorthand from './expandShorthand'; 5 | 6 | describe('expandShorthand', function () { 7 | it('test 1', function () { 8 | const result = expandShorthand({ 9 | name: String, 10 | count: Number, 11 | exp: /foo/, 12 | }); 13 | 14 | expect(result).to.deep.equal({ 15 | name: { 16 | type: String, 17 | }, 18 | count: { 19 | type: Number, 20 | }, 21 | exp: { 22 | type: String, 23 | regEx: /foo/, 24 | }, 25 | }); 26 | }); 27 | 28 | it('test 2', function () { 29 | const result = expandShorthand({ 30 | name: [String], 31 | count: [Number], 32 | }); 33 | 34 | expect(result).to.deep.equal({ 35 | name: { 36 | type: Array, 37 | }, 38 | 'name.$': { 39 | type: String, 40 | }, 41 | count: { 42 | type: Array, 43 | }, 44 | 'count.$': { 45 | type: Number, 46 | }, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /lib/utility/merge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We have relatively simple deep merging requirements in this package. 3 | * We are only ever merging messages config, so we know the structure, 4 | * we know there are no arrays, and we know there are no constructors 5 | * or weirdly defined properties. 6 | * 7 | * Thus, we can write a very simplistic deep merge function to avoid 8 | * pulling in a large dependency. 9 | */ 10 | 11 | export default function merge(destination, ...sources) { 12 | sources.forEach((source) => { 13 | Object.keys(source).forEach((prop) => { 14 | if (prop === '__proto__') return; // protect against prototype pollution 15 | if ( 16 | source[prop] 17 | && source[prop].constructor 18 | && source[prop].constructor === Object 19 | ) { 20 | if (!destination[prop] || !destination[prop].constructor || destination[prop].constructor !== Object) { 21 | destination[prop] = {}; 22 | } 23 | merge(destination[prop], source[prop]); 24 | } else { 25 | destination[prop] = source[prop]; 26 | } 27 | }); 28 | }); 29 | 30 | return destination; 31 | } 32 | -------------------------------------------------------------------------------- /lib/SimpleSchema_definition.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema, schemaDefinitionOptions } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema - Extend Schema Definition', function () { 7 | it('throws an error when the schema definition includes an unrecognized key', function () { 8 | expect(() => { 9 | const schema = new SimpleSchema({ // eslint-disable-line no-unused-vars 10 | name: { 11 | type: String, 12 | unique: true, 13 | }, 14 | }); 15 | }).to.throw(); 16 | }); 17 | 18 | it('does not throw an error when the schema definition includes a registered key', function () { 19 | SimpleSchema.extendOptions({ unique: true }); 20 | 21 | expect(() => { 22 | const schema = new SimpleSchema({ // eslint-disable-line no-unused-vars 23 | name: { 24 | type: String, 25 | unique: true, 26 | }, 27 | }); 28 | }).not.to.throw(); 29 | 30 | // Reset 31 | schemaDefinitionOptions.splice(schemaDefinitionOptions.indexOf('unique'), 1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/testHelpers/friendsSchema.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | const friendsSchema = new SimpleSchema({ 4 | name: { 5 | type: String, 6 | optional: true, 7 | }, 8 | friends: { 9 | type: Array, 10 | minCount: 1, 11 | }, 12 | 'friends.$': { 13 | type: Object, 14 | }, 15 | 'friends.$.name': { 16 | type: String, 17 | max: 3, 18 | }, 19 | 'friends.$.type': { 20 | type: String, 21 | allowedValues: ['best', 'good', 'bad'], 22 | }, 23 | 'friends.$.a': { 24 | type: Object, 25 | optional: true, 26 | }, 27 | 'friends.$.a.b': { 28 | type: SimpleSchema.Integer, 29 | optional: true, 30 | }, 31 | enemies: { 32 | type: Array, 33 | }, 34 | 'enemies.$': { 35 | type: Object, 36 | }, 37 | 'enemies.$.name': { 38 | type: String, 39 | }, 40 | 'enemies.$.traits': { 41 | type: Array, 42 | optional: true, 43 | }, 44 | 'enemies.$.traits.$': { 45 | type: Object, 46 | }, 47 | 'enemies.$.traits.$.name': { 48 | type: String, 49 | }, 50 | 'enemies.$.traits.$.weight': { 51 | type: Number, 52 | }, 53 | }); 54 | 55 | export default friendsSchema; 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-Today Eric Dobbertin; Meteor Community Packages 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/SimpleSchema_namedContext.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema - namedContext', function () { 7 | it('returns a named context', function () { 8 | const schema = new SimpleSchema({}); 9 | const context = schema.namedContext('form'); 10 | expect(context.name).to.equal('form'); 11 | expect(schema._validationContexts.form).to.equal(context); 12 | }); 13 | 14 | it('returns a context named "default" if no name is passed', function () { 15 | const schema = new SimpleSchema({}); 16 | const context = schema.namedContext(); 17 | expect(context.name).to.equal('default'); 18 | expect(schema._validationContexts.default).to.equal(context); 19 | }); 20 | 21 | it('returns the same context instance when called with the same name', function () { 22 | const schema = new SimpleSchema({}); 23 | const context1 = schema.namedContext('abc'); 24 | expect(schema._validationContexts.abc).to.equal(context1); 25 | const context2 = schema.namedContext('abc'); 26 | expect(context2).to.equal(context1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.5.2 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.2 # Packages for a great mobile UX 9 | mongo@2.0.1 # The database Meteor supports right now 10 | static-html@1.3.3 # Define static page content in .html files 11 | reactive-var@1.0.13 # Reactive variable for tracker 12 | tracker@1.3.4 # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css@1.9.3 # CSS minifier run for production mode 15 | standard-minifier-js@3.0.0 # JS minifier run for production mode 16 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers 17 | ecmascript@0.16.9 # Enable ECMAScript2015+ syntax in app code 18 | typescript@5.4.3 # Enable TypeScript syntax in .ts and .tsx modules 19 | shell-server@0.6.0 # Server-side component of the `meteor shell` command 20 | -------------------------------------------------------------------------------- /lib/main.tests.js: -------------------------------------------------------------------------------- 1 | import './clean/autoValue.tests' 2 | import './clean/convertToProperType.tests' 3 | import './clean/defaultValue.tests' 4 | import './clean/setAutoValues.tests' 5 | import './utility/getLastPartOfKey.tests' 6 | import './clean.tests' 7 | import './expandShorthand.tests' 8 | import './humanize.tests' 9 | import './SimpleSchema.tests' 10 | import './SimpleSchema_allowedValues.tests' 11 | import './SimpleSchema_autoValueFunctions.tests' 12 | import './SimpleSchema_blackbox.tests' 13 | import './SimpleSchema_custom.tests' 14 | import './SimpleSchema_definition.tests' 15 | import './SimpleSchema_extend.tests' 16 | import './SimpleSchema_getObjectSchema.tests' 17 | import './SimpleSchema_getQuickTypeForKey.tests' 18 | import './SimpleSchema_labels.tests' 19 | import './SimpleSchema_max.tests' 20 | import './SimpleSchema_messages.tests' 21 | import './SimpleSchema_min.tests' 22 | import './SimpleSchema_minCount.tests' 23 | import './SimpleSchema_namedContext.tests' 24 | import './SimpleSchema_omit.tests' 25 | import './SimpleSchema_oneOf.tests' 26 | import './SimpleSchema_pick.tests' 27 | import './SimpleSchema_regEx.tests' 28 | import './SimpleSchema_required.tests' 29 | import './SimpleSchema_rules.tests' 30 | import './SimpleSchema_type.tests' 31 | import './reactivity.tests' 32 | -------------------------------------------------------------------------------- /lib/humanize.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import humanize from './humanize'; 5 | 6 | describe('humanize', function () { 7 | it('works', function () { 8 | expect(humanize('super_snake_case')).to.equal('Super snake case'); 9 | expect(humanize('capitalizedCamelCase')).to.equal('Capitalized camel case'); 10 | expect(humanize('hyphen-case')).to.equal('Hyphen case'); 11 | expect(humanize('no-extensions-here.md')).to.equal('No extensions here'); 12 | expect(humanize('lower cased phrase')).to.equal('Lower cased phrase'); 13 | expect(humanize(' so many spaces ')).to.equal('So many spaces'); 14 | expect(humanize(123)).to.equal('123'); 15 | expect(humanize('')).to.equal(''); 16 | expect(humanize(null)).to.equal(''); 17 | expect(humanize(undefined)).to.equal(''); 18 | expect(humanize('externalSource')).to.equal('External source'); 19 | expect(humanize('externalSourceId')).to.equal('External source ID'); 20 | expect(humanize('externalSource_id')).to.equal('External source ID'); 21 | expect(humanize('_id')).to.equal('ID'); 22 | // Make sure it does not mess with "id" in the middle of a word 23 | expect(humanize('overridden')).to.equal('Overridden'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/humanize.js: -------------------------------------------------------------------------------- 1 | /* 2 | Code source: 3 | https://github.com/jxson/string-humanize 4 | https://github.com/jxson/string-capitalize 5 | */ 6 | 7 | function capitalize(text) { 8 | text = text || ''; 9 | text = text.trim(); 10 | 11 | if (text[0]) { 12 | text = text[0].toUpperCase() + text.substr(1).toLowerCase(); 13 | } 14 | 15 | // Do "ID" instead of "id" or "Id" 16 | text = text.replace(/\bid\b/g, 'ID'); 17 | text = text.replace(/\bId\b/g, 'ID'); 18 | 19 | return text; 20 | } 21 | 22 | function underscore(text) { 23 | text = text || ''; 24 | text = text.toString(); // might be a number 25 | text = text.trim(); 26 | text = text.replace(/([a-z\d])([A-Z]+)/g, '$1_$2'); 27 | text = text.replace(/[-\s]+/g, '_').toLowerCase(); 28 | 29 | return text; 30 | } 31 | 32 | function extname(text) { 33 | const index = text.lastIndexOf('.'); 34 | const ext = text.substring(index, text.length); 35 | 36 | return (index === -1) ? '' : ext; 37 | } 38 | 39 | function humanize(text) { 40 | text = text || ''; 41 | text = text.toString(); // might be a number 42 | text = text.trim(); 43 | text = text.replace(extname(text), ''); 44 | text = underscore(text); 45 | text = text.replace(/[\W_]+/g, ' '); 46 | 47 | return capitalize(text); 48 | } 49 | 50 | export default humanize; 51 | -------------------------------------------------------------------------------- /lib/testHelpers/requiredSchema.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | const requiredSchema = new SimpleSchema({ 4 | requiredString: { 5 | type: String, 6 | }, 7 | requiredBoolean: { 8 | type: Boolean, 9 | }, 10 | requiredNumber: { 11 | type: SimpleSchema.Integer, 12 | }, 13 | requiredDate: { 14 | type: Date, 15 | }, 16 | requiredEmail: { 17 | type: String, 18 | regEx: SimpleSchema.RegEx.Email, 19 | }, 20 | requiredUrl: { 21 | type: String, 22 | custom() { 23 | if (!this.isSet) return; 24 | try { 25 | new URL(this.value); // eslint-disable-line 26 | } catch (err) { 27 | return 'badUrl'; 28 | } 29 | }, 30 | }, 31 | requiredObject: { 32 | type: Object, 33 | }, 34 | 'requiredObject.requiredNumber': { 35 | type: SimpleSchema.Integer, 36 | }, 37 | optionalObject: { 38 | type: Object, 39 | optional: true, 40 | }, 41 | 'optionalObject.requiredString': { 42 | type: String, 43 | }, 44 | anOptionalOne: { 45 | type: String, 46 | optional: true, 47 | min: 20, 48 | }, 49 | }); 50 | 51 | requiredSchema.messageBox.messages({ 52 | 'regEx requiredEmail': '[label] is not a valid email address', 53 | 'regEx requiredUrl': '[label] is not a valid URL', 54 | }); 55 | 56 | export default requiredSchema; 57 | -------------------------------------------------------------------------------- /.github/workflows/new-issue-comment.yml: -------------------------------------------------------------------------------- 1 | name: Add immediate comment on new issues 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | createComment: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Comment 12 | uses: peter-evans/create-or-update-comment@v1.4.2 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | Thank you for submitting this issue! 17 | 18 | We, the Members of Meteor Community Packages take every issue seriously. 19 | Our goal is to provide long-term lifecycles for packages and keep up 20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem. 21 | 22 | However, we contribute to these packages mostly in our free time. 23 | Therefore, we can't guarantee your issues to be solved within certain time. 24 | 25 | If you think this issue is trivial to solve, don't hesitate to submit 26 | a pull request, too! We will accompany you in the process with reviews and hints 27 | on how to get development set up. 28 | 29 | Please also consider sponsoring the maintainers of the package. 30 | If you don't know who is currently maintaining this package, just leave a comment 31 | and we'll let you know 32 | -------------------------------------------------------------------------------- /lib/clean/convertToProperType.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import convertToProperType from './convertToProperType'; 5 | 6 | describe('convertToProperType', function () { 7 | it('convert string `false` to boolean value false', function () { 8 | expect(convertToProperType('false', Boolean)).to.equal(false); 9 | }); 10 | 11 | it('convert string `FALSE` to boolean value false', function () { 12 | expect(convertToProperType('FALSE', Boolean)).to.equal(false); 13 | }); 14 | 15 | it('convert string `true` to boolean value true', function () { 16 | expect(convertToProperType('true', Boolean)).to.equal(true); 17 | }); 18 | 19 | it('convert string `TRUE` to boolean value true', function () { 20 | expect(convertToProperType('TRUE', Boolean)).to.equal(true); 21 | }); 22 | 23 | it('convert number 1 to boolean value true', function () { 24 | expect(convertToProperType(1, Boolean)).to.equal(true); 25 | }); 26 | 27 | it('convert number 0 to boolean value false', function () { 28 | expect(convertToProperType(0, Boolean)).to.equal(false); 29 | }); 30 | 31 | it('don\'t convert NaN to boolean value', function () { 32 | expect(convertToProperType(Number('text'), Boolean)).to.deep.equal(NaN); 33 | }); 34 | 35 | it('does not try to convert null', function () { 36 | expect(convertToProperType(null, Array)).to.equal(null); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/SimpleSchema_pick.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | it('pick', function () { 8 | const schema = new SimpleSchema({ 9 | foo: { type: Object }, 10 | 'foo.bar': { type: String }, 11 | fooArray: { type: Array }, 12 | 'fooArray.$': { type: Object }, 13 | 'fooArray.$.bar': { type: String }, 14 | }); 15 | 16 | let newSchema = schema.pick('foo'); 17 | expect(Object.keys(newSchema.schema())).to.deep.equal(['foo', 'foo.bar']); 18 | 19 | newSchema = schema.pick('fooArray'); 20 | expect(Object.keys(newSchema.schema())).to.deep.equal(['fooArray', 'fooArray.$', 'fooArray.$.bar']); 21 | 22 | newSchema = schema.pick('foo', 'fooArray'); 23 | expect(Object.keys(newSchema.schema())).to.deep.equal(['foo', 'foo.bar', 'fooArray', 'fooArray.$', 'fooArray.$.bar']); 24 | 25 | newSchema = schema.pick('blah'); 26 | expect(Object.keys(newSchema.schema())).to.deep.equal([]); 27 | }); 28 | 29 | it('error when you do not pick the parent', () => { 30 | const schema = new SimpleSchema({ 31 | level1: { type: Object }, 32 | 'level1.level2': { type: Boolean }, 33 | }); 34 | 35 | expect(() => { 36 | schema.pick('level1.level2'); 37 | }).to.throw('"level1.level2" is in the schema but "level1" is not'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/validation/typeValidator/doStringChecks.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../../SimpleSchema'; 2 | 3 | export default function doStringChecks(def, keyValue) { 4 | // Is it a String? 5 | if (typeof keyValue !== 'string') { 6 | return { type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, dataType: 'String' }; 7 | } 8 | 9 | // Is the string too long? 10 | if (def.max !== null && def.max < keyValue.length) { 11 | return { type: SimpleSchema.ErrorTypes.MAX_STRING, max: def.max }; 12 | } 13 | 14 | // Is the string too short? 15 | if (def.min !== null && def.min > keyValue.length) { 16 | return { type: SimpleSchema.ErrorTypes.MIN_STRING, min: def.min }; 17 | } 18 | 19 | // Does the string match the regular expression? 20 | if ( 21 | (def.skipRegExCheckForEmptyStrings !== true || keyValue !== '') 22 | && def.regEx instanceof RegExp && !def.regEx.test(keyValue) 23 | ) { 24 | return { type: SimpleSchema.ErrorTypes.FAILED_REGULAR_EXPRESSION, regExp: def.regEx.toString() }; 25 | } 26 | 27 | // If regEx is an array of regular expressions, does the string match all of them? 28 | if (Array.isArray(def.regEx)) { 29 | let regExError; 30 | def.regEx.every((re) => { 31 | if (!re.test(keyValue)) { 32 | regExError = { type: SimpleSchema.ErrorTypes.FAILED_REGULAR_EXPRESSION, regExp: re.toString() }; 33 | return false; 34 | } 35 | return true; 36 | }); 37 | if (regExError) return regExError; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/validation/typeValidator/doNumberChecks.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../../SimpleSchema'; 2 | 3 | // Polyfill to support IE11 4 | Number.isInteger = Number.isInteger || function isInteger(value) { 5 | return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; 6 | }; 7 | 8 | export default function doNumberChecks(def, keyValue, op, expectsInteger) { 9 | // Is it a valid number? 10 | if (typeof keyValue !== 'number' || isNaN(keyValue)) { 11 | return { type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, dataType: expectsInteger ? 'Integer' : 'Number' }; 12 | } 13 | 14 | // Assuming we are not incrementing, is the value less than the maximum value? 15 | if (op !== '$inc' && def.max !== null && (def.exclusiveMax ? def.max <= keyValue : def.max < keyValue)) { 16 | return { type: def.exclusiveMax ? SimpleSchema.ErrorTypes.MAX_NUMBER_EXCLUSIVE : SimpleSchema.ErrorTypes.MAX_NUMBER, max: def.max }; 17 | } 18 | 19 | // Assuming we are not incrementing, is the value more than the minimum value? 20 | if (op !== '$inc' && def.min !== null && (def.exclusiveMin ? def.min >= keyValue : def.min > keyValue)) { 21 | return { type: def.exclusiveMin ? SimpleSchema.ErrorTypes.MIN_NUMBER_EXCLUSIVE : SimpleSchema.ErrorTypes.MIN_NUMBER, min: def.min }; 22 | } 23 | 24 | // Is it an integer if we expect an integer? 25 | if (expectsInteger && !Number.isInteger(keyValue)) { 26 | return { type: SimpleSchema.ErrorTypes.MUST_BE_INTEGER }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/SimpleSchema_blackbox.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import expectErrorLength from './testHelpers/expectErrorLength'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | const schema = new SimpleSchema({ 7 | blackBoxObject: { 8 | type: Object, 9 | optional: true, 10 | blackbox: true, 11 | }, 12 | }); 13 | 14 | describe('SimpleSchema - blackbox', function () { 15 | it('allows an empty object', function () { 16 | expectErrorLength(schema, { 17 | blackBoxObject: {}, 18 | }).to.deep.equal(0); 19 | }); 20 | 21 | it('allows any properties', function () { 22 | expectErrorLength(schema, { 23 | blackBoxObject: { 24 | foo: 'bar', 25 | }, 26 | }).to.deep.equal(0); 27 | }); 28 | 29 | it('allows any properties on $set object', function () { 30 | expectErrorLength(schema, { 31 | $set: { 32 | blackBoxObject: { 33 | foo: 'bar', 34 | }, 35 | }, 36 | }, { modifier: true }).to.deep.equal(0); 37 | }); 38 | 39 | it('allows to $set any subobject', function () { 40 | expectErrorLength(schema, { 41 | $set: { 42 | 'blackBoxObject.foo': 'bar', 43 | }, 44 | }, { modifier: true }).to.deep.equal(0); 45 | 46 | expectErrorLength(schema, { 47 | $set: { 48 | 'blackBoxObject.1': 'bar', 49 | }, 50 | }, { modifier: true }).to.deep.equal(0); 51 | }); 52 | 53 | it('allows to $push into any subobject', function () { 54 | expectErrorLength(schema, { 55 | $push: { 56 | 'blackBoxObject.foo': 'bar', 57 | }, 58 | }, { modifier: true }).to.deep.equal(0); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@2.0.0 2 | autoupdate@2.0.0 3 | babel-compiler@7.11.0 4 | babel-runtime@1.5.2 5 | base64@1.0.13 6 | binary-heap@1.0.12 7 | blaze-tools@2.0.0-rc300.2 8 | boilerplate-generator@2.0.0 9 | caching-compiler@2.0.0 10 | caching-html-compiler@2.0.0-rc300.2 11 | callback-hook@1.6.0 12 | check@1.4.2 13 | core-runtime@1.0.0 14 | ddp@1.4.2 15 | ddp-client@3.0.1 16 | ddp-common@1.4.4 17 | ddp-server@3.0.1 18 | diff-sequence@1.1.3 19 | dynamic-import@0.7.4 20 | ecmascript@0.16.9 21 | ecmascript-runtime@0.8.2 22 | ecmascript-runtime-client@0.12.2 23 | ecmascript-runtime-server@0.11.1 24 | ejson@1.1.4 25 | es5-shim@4.8.1 26 | facts-base@1.0.2 27 | fetch@0.1.5 28 | geojson-utils@1.0.12 29 | hot-code-push@1.0.5 30 | html-tools@2.0.0-rc300.2 31 | htmljs@2.0.0-rc300.2 32 | id-map@1.2.0 33 | inter-process-messaging@0.1.2 34 | launch-screen@2.0.1 35 | logging@1.3.5 36 | meteor@2.0.1 37 | meteor-base@1.5.2 38 | minifier-css@2.0.0 39 | minifier-js@3.0.0 40 | minimongo@2.0.1 41 | mobile-experience@1.1.2 42 | mobile-status-bar@1.1.1 43 | modern-browsers@0.1.11 44 | modules@0.20.1 45 | modules-runtime@0.13.2 46 | mongo@2.0.1 47 | mongo-decimal@0.1.4-beta300.7 48 | mongo-dev-server@1.1.1 49 | mongo-id@1.0.9 50 | npm-mongo@4.17.4 51 | ordered-dict@1.2.0 52 | promise@1.0.0 53 | random@1.2.2 54 | react-fast-refresh@0.2.9 55 | reactive-var@1.0.13 56 | reload@1.3.2 57 | retry@1.1.1 58 | routepolicy@1.1.2 59 | shell-server@0.6.0 60 | socket-stream-client@0.5.3 61 | spacebars-compiler@2.0.0-rc300.2 62 | standard-minifier-css@1.9.3 63 | standard-minifier-js@3.0.0 64 | static-html@1.3.3 65 | templating-tools@2.0.0-rc300.2 66 | tracker@1.3.4 67 | typescript@5.4.3 68 | underscore@1.6.4 69 | webapp@2.0.1 70 | webapp-hashing@1.1.2 71 | -------------------------------------------------------------------------------- /lib/expandShorthand.js: -------------------------------------------------------------------------------- 1 | import MongoObject from 'mongo-object'; 2 | 3 | /** 4 | * Clones a schema object, expanding shorthand as it does it. 5 | */ 6 | function expandShorthand(schema) { 7 | const schemaClone = {}; 8 | 9 | Object.keys(schema).forEach((key) => { 10 | const definition = schema[key]; 11 | // CASE 1: Not shorthand. Just clone 12 | if (MongoObject.isBasicObject(definition)) { 13 | schemaClone[key] = { ...definition }; 14 | return; 15 | } 16 | 17 | // CASE 2: The definition is an array of some type 18 | if (Array.isArray(definition)) { 19 | if (Array.isArray(definition[0])) { 20 | throw new Error(`Array shorthand may only be used to one level of depth (${key})`); 21 | } 22 | const type = definition[0]; 23 | schemaClone[key] = { type: Array }; 24 | 25 | // Also add the item key definition 26 | const itemKey = `${key}.$`; 27 | if (schema[itemKey]) { 28 | throw new Error(`Array shorthand used for ${key} field but ${key}.$ key is already in the schema`); 29 | } 30 | 31 | if (type instanceof RegExp) { 32 | schemaClone[itemKey] = { type: String, regEx: type }; 33 | } else { 34 | schemaClone[itemKey] = { type }; 35 | } 36 | return; 37 | } 38 | 39 | // CASE 3: The definition is a regular expression 40 | if (definition instanceof RegExp) { 41 | schemaClone[key] = { 42 | type: String, 43 | regEx: definition, 44 | }; 45 | return; 46 | } 47 | 48 | // CASE 4: The definition is something, a type 49 | schemaClone[key] = { type: definition }; 50 | }); 51 | 52 | return schemaClone; 53 | } 54 | 55 | export default expandShorthand; 56 | -------------------------------------------------------------------------------- /.github/workflows/lint-test-publish.yml: -------------------------------------------------------------------------------- 1 | # the test suite runs the tests (headless, server+client) for multiple Meteor releases 2 | name: Test suite 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | # lint: 11 | # name: Javascript standard lint 12 | # runs-on: ubuntu-latest 13 | # steps: 14 | # - name: checkout 15 | # uses: actions/checkout@v4 16 | # 17 | # - name: setup node 18 | # uses: actions/setup-node@v4 19 | # with: 20 | # node-version: 20 21 | # 22 | # - name: cache dependencies 23 | # uses: actions/cache@v4 24 | # with: 25 | # path: ~/.npm 26 | # key: ${{ runner.os }}-node-20-${{ hashFiles('**/package-lock.json') }} 27 | # restore-keys: | 28 | # ${{ runner.os }}-node-20- 29 | # 30 | # - run: cd tests && npm ci && npm run setup && npm run lint 31 | 32 | test: 33 | name: Meteor package tests 34 | # needs: [lint] 35 | runs-on: ubuntu-latest 36 | strategy: 37 | matrix: 38 | meteorRelease: 39 | - '3.0.2' 40 | # Latest version 41 | steps: 42 | - name: Checkout code 43 | uses: actions/checkout@v4 44 | 45 | - name: Install Node.js 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: 20 49 | 50 | - name: Setup meteor ${{ matrix.meteorRelease }} 51 | uses: meteorengineer/setup-meteor@v1 52 | with: 53 | meteor-release: ${{ matrix.meteorRelease }} 54 | 55 | - name: cache dependencies 56 | uses: actions/cache@v4 57 | with: 58 | path: ~/.npm 59 | key: ${{ runner.os }}-node-20-${{ hashFiles('**/package-lock.json') }} 60 | restore-keys: | 61 | ${{ runner.os }}-node-20- 62 | 63 | - run: cd tests && npm ci && npm run setup && npm run test 64 | -------------------------------------------------------------------------------- /lib/clean/setAutoValues.tests.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { SimpleSchema } from '../SimpleSchema'; 3 | import { sortAutoValueFunctions } from './setAutoValues'; 4 | 5 | describe('setAutoValues', () => { 6 | it('sorts correctly', () => { 7 | const schema = new SimpleSchema({ 8 | field1: { 9 | type: String, 10 | autoValue() {}, 11 | }, 12 | field2: { 13 | type: String, 14 | autoValue() {}, 15 | }, 16 | field3: { 17 | type: Number, 18 | autoValue() {}, 19 | }, 20 | nested: Object, 21 | 'nested.field1': { 22 | type: String, 23 | autoValue() {}, 24 | }, 25 | 'nested.field2': { 26 | type: String, 27 | autoValue() {}, 28 | }, 29 | 'nested.field3': { 30 | type: String, 31 | autoValue() {}, 32 | }, 33 | 'nested.field4': { 34 | type: String, 35 | defaultValue: 'test', 36 | }, 37 | field4: { 38 | type: Number, 39 | autoValue() {}, 40 | }, 41 | field5: { 42 | type: Number, 43 | autoValue() {}, 44 | }, 45 | field6: { 46 | type: String, 47 | autoValue() {}, 48 | }, 49 | field7: { 50 | type: String, 51 | autoValue() {}, 52 | }, 53 | }); 54 | 55 | const autoValueFunctions = schema.autoValueFunctions(); 56 | const sorted = sortAutoValueFunctions(autoValueFunctions); 57 | 58 | const FIELD_COUNT = 7; 59 | const NESTED_FIELD_COUNT = 4; 60 | // expecting: field1, field2, ..., field7, nested.field1, ... nested.field4 61 | const fieldOrder = sorted.map(({ fieldName }) => fieldName); 62 | for (let i = 0; i < FIELD_COUNT; ++i) { 63 | expect(fieldOrder[i]).to.equal(`field${i + 1}`); 64 | } 65 | for (let i = FIELD_COUNT; i < FIELD_COUNT + NESTED_FIELD_COUNT; ++i) { 66 | expect(fieldOrder[i]).to.equal(`nested.field${i - FIELD_COUNT + 1}`); 67 | } 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IDE specific 107 | .idea 108 | .vscode 109 | .atom -------------------------------------------------------------------------------- /lib/clean/convertToProperType.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | 3 | /** 4 | * Converts value to proper type 5 | * 6 | * @param {Any} value Value to try to convert 7 | * @param {Any} type A type 8 | * @returns {Any} Value converted to type. 9 | */ 10 | function convertToProperType(value, type) { 11 | // Can't and shouldn't convert arrays or objects or null 12 | if ( 13 | Array.isArray(value) 14 | || (value && (typeof value === 'function' || typeof value === 'object') && !(value instanceof Date)) 15 | || value === null 16 | ) return value; 17 | 18 | // Convert to String type 19 | if (type === String) { 20 | if (value === null || value === undefined) return value; 21 | return value.toString(); 22 | } 23 | 24 | // Convert to Number type 25 | if (type === Number || type === SimpleSchema.Integer) { 26 | if (typeof value === 'string' && value.length > 0) { 27 | // Try to convert numeric strings to numbers 28 | const numberVal = Number(value); 29 | if (!isNaN(numberVal)) return numberVal; 30 | } 31 | // Leave it; will fail validation 32 | return value; 33 | } 34 | 35 | // If target type is a Date we can safely convert from either a 36 | // number (Integer value representing the number of milliseconds 37 | // since 1 January 1970 00:00:00 UTC) or a string that can be parsed 38 | // by Date. 39 | if (type === Date) { 40 | if (typeof value === 'string') { 41 | const parsedDate = Date.parse(value); 42 | if (isNaN(parsedDate) === false) return new Date(parsedDate); 43 | } 44 | if (typeof value === 'number') return new Date(value); 45 | } 46 | 47 | // Convert to Boolean type 48 | if (type === Boolean) { 49 | if (typeof value === 'string') { 50 | // Convert exact string 'true' and 'false' to true and false respectively 51 | if (value.toLowerCase() === 'true') return true; 52 | if (value.toLowerCase() === 'false') return false; 53 | } else if (typeof value === 'number' && !isNaN(value)) { // NaN can be error, so skipping it 54 | return Boolean(value); 55 | } 56 | } 57 | 58 | // If an array is what you want, I'll give you an array 59 | if (type === Array) return [value]; 60 | 61 | // Could not convert 62 | return value; 63 | } 64 | 65 | export default convertToProperType; 66 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint-env meteor */ 2 | Package.describe({ 3 | name: 'aldeed:simple-schema', 4 | summary: 'A simple schema validation object with reactivity. Used by collection2 and autoform.', 5 | version: '2.0.0', 6 | git: 'https://github.com/aldeed/meteor-simple-schema.git' 7 | }); 8 | 9 | Npm.depends({ 10 | 'mongo-object': '3.0.1', 11 | 'message-box': '0.2.7', 12 | 'clone': '2.1.2' 13 | }); 14 | 15 | Package.onUse(function (api) { 16 | api.versionsFrom(['2.3', '2.8.0', '3.0']); 17 | api.use('ecmascript'); 18 | api.mainModule('lib/main.js'); 19 | }); 20 | 21 | Package.onTest(function (api) { 22 | api.versionsFrom(['2.3', '2.8.0', '3.0']); 23 | api.use([ 24 | //'lmieulet:meteor-legacy-coverage@0.1.0', 25 | //'lmieulet:meteor-coverage@4.0.0', 26 | 'meteortesting:mocha@2.0.0 || 3.2.0', 27 | 'ecmascript', 28 | 'tracker', 29 | // 'mongo', 30 | 'aldeed:simple-schema@2.0.0' 31 | ]) 32 | 33 | api.addFiles([ 34 | 'lib/clean/autoValue.tests.js', 35 | 'lib/clean/convertToProperType.tests.js', 36 | 'lib/clean/defaultValue.tests.js', 37 | 'lib/clean/setAutoValues.tests.js', 38 | 'lib/utility/getLastPartOfKey.tests.js', 39 | 'lib/clean.tests.js', 40 | 'lib/expandShorthand.tests.js', 41 | 'lib/humanize.tests.js', 42 | 'lib/SimpleSchema.tests.js', 43 | 'lib/SimpleSchema_allowedValues.tests.js', 44 | 'lib/SimpleSchema_autoValueFunctions.tests.js', 45 | 'lib/SimpleSchema_blackbox.tests.js', 46 | 'lib/SimpleSchema_custom.tests.js', 47 | 'lib/SimpleSchema_definition.tests.js', 48 | 'lib/SimpleSchema_extend.tests.js', 49 | 'lib/SimpleSchema_getObjectSchema.tests.js', 50 | 'lib/SimpleSchema_getQuickTypeForKey.tests.js', 51 | 'lib/SimpleSchema_labels.tests.js', 52 | 'lib/SimpleSchema_max.tests.js', 53 | 'lib/SimpleSchema_messages.tests.js', 54 | 'lib/SimpleSchema_min.tests.js', 55 | 'lib/SimpleSchema_minCount.tests.js', 56 | 'lib/SimpleSchema_namedContext.tests.js', 57 | 'lib/SimpleSchema_omit.tests.js', 58 | 'lib/SimpleSchema_oneOf.tests.js', 59 | 'lib/SimpleSchema_pick.tests.js', 60 | 'lib/SimpleSchema_regEx.tests.js', 61 | 'lib/SimpleSchema_required.tests.js', 62 | 'lib/SimpleSchema_rules.tests.js', 63 | 'lib/SimpleSchema_type.tests.js', 64 | 'lib/reactivity.tests.js' 65 | ]); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/validation/typeValidator/index.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../../SimpleSchema'; 2 | import doDateChecks from './doDateChecks'; 3 | import doNumberChecks from './doNumberChecks'; 4 | import doStringChecks from './doStringChecks'; 5 | import doArrayChecks from './doArrayChecks'; 6 | 7 | export default function typeValidator() { 8 | if (!this.valueShouldBeChecked) return; 9 | 10 | const def = this.definition; 11 | const expectedType = def.type; 12 | const keyValue = this.value; 13 | const op = this.operator; 14 | 15 | if (expectedType === String) return doStringChecks(def, keyValue); 16 | if (expectedType === Number) return doNumberChecks(def, keyValue, op, false); 17 | if (expectedType === SimpleSchema.Integer) return doNumberChecks(def, keyValue, op, true); 18 | 19 | if (expectedType === Boolean) { 20 | // Is it a boolean? 21 | if (typeof keyValue === 'boolean') return; 22 | return { type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, dataType: 'Boolean' }; 23 | } 24 | 25 | if (expectedType === Object || SimpleSchema.isSimpleSchema(expectedType)) { 26 | // Is it an object? 27 | if ( 28 | keyValue === Object(keyValue) 29 | && typeof keyValue[Symbol.iterator] !== 'function' 30 | && !(keyValue instanceof Date) 31 | ) return; 32 | return { type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, dataType: 'Object' }; 33 | } 34 | 35 | if (expectedType === Array) return doArrayChecks(def, keyValue); 36 | 37 | if (expectedType instanceof Function) { 38 | // Generic constructor checks 39 | if (!(keyValue instanceof expectedType)) { 40 | // https://docs.mongodb.com/manual/reference/operator/update/currentDate/ 41 | const dateTypeIsOkay = expectedType === Date 42 | && op === '$currentDate' 43 | && (keyValue === true || JSON.stringify(keyValue) === '{"$type":"date"}'); 44 | 45 | if (expectedType !== Date || !dateTypeIsOkay) { 46 | return { 47 | type: SimpleSchema.ErrorTypes.EXPECTED_TYPE, 48 | dataType: expectedType.name, 49 | }; 50 | } 51 | } 52 | 53 | // Date checks 54 | if (expectedType === Date) { 55 | // https://docs.mongodb.com/manual/reference/operator/update/currentDate/ 56 | if (op === '$currentDate') { 57 | return doDateChecks(def, new Date()); 58 | } 59 | return doDateChecks(def, keyValue); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/SimpleSchema_labels.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema - label', function () { 7 | it('inflection', function () { 8 | const schema = new SimpleSchema({ 9 | minMaxNumber: { type: SimpleSchema.Integer }, 10 | obj: { type: Object }, 11 | 'obj.someString': { type: String }, 12 | }); 13 | 14 | expect(schema.label('minMaxNumber')).to.equal('Min max number'); 15 | expect(schema.label('obj.someString')).to.equal('Some string'); 16 | }); 17 | 18 | it('dynamic', function () { 19 | const schema = new SimpleSchema({ 20 | minMaxNumber: { type: SimpleSchema.Integer }, 21 | obj: { type: Object }, 22 | 'obj.someString': { type: String }, 23 | }); 24 | 25 | expect(schema.label('obj.someString')).to.equal('Some string'); 26 | 27 | schema.labels({ 28 | 'obj.someString': 'A different label', 29 | }); 30 | 31 | expect(schema.label('obj.someString')).to.equal('A different label'); 32 | }); 33 | 34 | it('callback', function () { 35 | const schema = new SimpleSchema({ 36 | minMaxNumber: { type: SimpleSchema.Integer }, 37 | obj: { type: Object }, 38 | 'obj.someString': { type: String }, 39 | }); 40 | 41 | expect(schema.label('obj.someString')).to.equal('Some string'); 42 | 43 | schema.labels({ 44 | 'obj.someString': () => 'A callback label', 45 | }); 46 | 47 | expect(schema.label('obj.someString')).to.equal('A callback label'); 48 | }); 49 | 50 | it('should allow apostrophes ("\'") in labels', () => { 51 | const schema = new SimpleSchema({ 52 | foo: { 53 | type: String, 54 | label: 'Manager/supervisor\'s name', 55 | }, 56 | }); 57 | expect(schema.label('foo')).to.equal('Manager/supervisor\'s name'); 58 | }); 59 | 60 | it('can set label of field in nested schema', function () { 61 | const objSchema = new SimpleSchema({ 62 | someString: String, 63 | }); 64 | 65 | const schema = new SimpleSchema({ 66 | obj: objSchema, 67 | }); 68 | 69 | expect(schema.label('obj.someString')).to.equal('Some string'); 70 | 71 | schema.labels({ 72 | 'obj.someString': 'New label', 73 | }); 74 | 75 | expect(schema.label('obj.someString')).to.equal('New label'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testapp", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run", 6 | "setup": "mkdir -p packages && ln -sfn ../../ ./packages/meteor-simple-schema", 7 | "lint": "standardx -v ../ | snazzy", 8 | "lint:fix": "standardx --fix ../ | snazzy", 9 | "test": "TEST_BROWSER_DRIVER=puppeteer METEOR_PACKAGE_DIRS='../:../../' TEST_BROWSER_DRIVER=puppeteer meteor test-packages --once --raw-logs --driver-package meteortesting:mocha ../", 10 | "test:watch": "TEST_BROWSER_DRIVER=puppeteer METEOR_PACKAGE_DIRS='../:../../' TEST_WATCH=1 meteor test-packages --raw-logs --driver-package meteortesting:mocha ../", 11 | "test:browser": "METEOR_PACKAGE_DIRS='../:../../' TEST_WATCH=1 meteor test-packages --raw-logs --driver-package meteortesting:mocha ../", 12 | "test:coverage": "TEST_BROWSER_DRIVER=puppeteer METEOR_PACKAGE_DIRS='../:../../' TEST_CLIENT=1 TEST_SERVER=1 COVERAGE=1 COVERAGE_OUT_JSON=1 COVERAGE_OUT_HTML=1 COVERAGE_APP_FOLDER=$(pwd)/ meteor test-packages --raw-logs --once --driver-package meteortesting:mocha ./packages/simple-schema", 13 | "report": "nyc report -t .coverage" 14 | }, 15 | "dependencies": { 16 | "@babel/runtime": "^7.26.10", 17 | "meteor-node-stubs": "^1.2.9" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.12.7", 21 | "@babel/eslint-parser": "^7.12.1", 22 | "babel-plugin-istanbul": "^6.1.1", 23 | "chai": "^5.0.0", 24 | "eslint-config-standard": "^16.0.2", 25 | "nyc": "^15.1.0", 26 | "puppeteer": "^23.1.0", 27 | "sinon": "^9.2.1", 28 | "snazzy": "^9.0.0", 29 | "standardx": "^7.0.0" 30 | }, 31 | "babel": { 32 | "env": { 33 | "COVERAGE": { 34 | "plugins": [ 35 | "istanbul" 36 | ] 37 | } 38 | } 39 | }, 40 | "standardx": { 41 | "globals": [ 42 | "AutoForm", 43 | "arrayTracker", 44 | "globalDefaultTemplate", 45 | "defaultTypeTemplates", 46 | "deps" 47 | ], 48 | "ignore": [ 49 | "**/tests/" 50 | ] 51 | }, 52 | "eslintConfig": { 53 | "parser": "@babel/eslint-parser", 54 | "parserOptions": { 55 | "sourceType": "module", 56 | "allowImportExportEverywhere": false 57 | }, 58 | "rules": { 59 | "brace-style": [ 60 | "error", 61 | "stroustrup", 62 | { 63 | "allowSingleLine": true 64 | } 65 | ] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/defaultMessages.js: -------------------------------------------------------------------------------- 1 | import regExpObj from './regExp'; 2 | 3 | const regExpMessages = [ 4 | { exp: regExpObj.Email, msg: 'must be a valid email address' }, 5 | { exp: regExpObj.EmailWithTLD, msg: 'must be a valid email address' }, 6 | { exp: regExpObj.Domain, msg: 'must be a valid domain' }, 7 | { exp: regExpObj.WeakDomain, msg: 'must be a valid domain' }, 8 | { exp: regExpObj.IP, msg: 'must be a valid IPv4 or IPv6 address' }, 9 | { exp: regExpObj.IPv4, msg: 'must be a valid IPv4 address' }, 10 | { exp: regExpObj.IPv6, msg: 'must be a valid IPv6 address' }, 11 | { exp: regExpObj.Url, msg: 'must be a valid URL' }, 12 | { exp: regExpObj.Id, msg: 'must be a valid alphanumeric ID' }, 13 | { exp: regExpObj.ZipCode, msg: 'must be a valid ZIP code' }, 14 | { exp: regExpObj.Phone, msg: 'must be a valid phone number' }, 15 | ]; 16 | 17 | const defaultMessages = { 18 | initialLanguage: 'en', 19 | messages: { 20 | en: { 21 | required: '{{{label}}} is required', 22 | minString: '{{{label}}} must be at least {{min}} characters', 23 | maxString: '{{{label}}} cannot exceed {{max}} characters', 24 | minNumber: '{{{label}}} must be at least {{min}}', 25 | maxNumber: '{{{label}}} cannot exceed {{max}}', 26 | minNumberExclusive: '{{{label}}} must be greater than {{min}}', 27 | maxNumberExclusive: '{{{label}}} must be less than {{max}}', 28 | minDate: '{{{label}}} must be on or after {{min}}', 29 | maxDate: '{{{label}}} cannot be after {{max}}', 30 | badDate: '{{{label}}} is not a valid date', 31 | minCount: 'You must specify at least {{minCount}} values', 32 | maxCount: 'You cannot specify more than {{maxCount}} values', 33 | noDecimal: '{{{label}}} must be an integer', 34 | notAllowed: '{{{value}}} is not an allowed value', 35 | expectedType: '{{{label}}} must be of type {{dataType}}', 36 | regEx({ 37 | label, 38 | regExp, 39 | }) { 40 | // See if there's one where exp matches this expression 41 | let msgObj; 42 | if (regExp) { 43 | msgObj = regExpMessages.find((o) => o.exp && o.exp.toString() === regExp); 44 | } 45 | 46 | const regExpMessage = msgObj ? msgObj.msg : 'failed regular expression validation'; 47 | 48 | return `${label} ${regExpMessage}`; 49 | }, 50 | keyNotInSchema: '{{name}} is not allowed by the schema', 51 | }, 52 | }, 53 | }; 54 | 55 | export default defaultMessages; 56 | -------------------------------------------------------------------------------- /lib/validation/requiredValidator.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | import { getKeysWithValueInObj } from '../utility'; 3 | 4 | // Check for missing required values. The general logic is this: 5 | // * If the operator is $unset or $rename, it's invalid. 6 | // * If the value is null, it's invalid. 7 | // * If the value is undefined and one of the following are true, it's invalid: 8 | // * We're validating a key of a sub-object. 9 | // * We're validating a key of an object that is an array item. 10 | // * We're validating a document (as opposed to a modifier). 11 | // * We're validating a key under the $set operator in a modifier, and it's an upsert. 12 | export default function requiredValidator() { 13 | const { 14 | definition, isInArrayItemObject, isInSubObject, key, obj, operator, value, 15 | } = this; 16 | const { optional } = definition; 17 | 18 | if (optional) return; 19 | 20 | // If value is null, no matter what, we add required 21 | if (value === null) return SimpleSchema.ErrorTypes.REQUIRED; 22 | 23 | // If operator would remove, we add required 24 | if (operator === '$unset' || operator === '$rename') return SimpleSchema.ErrorTypes.REQUIRED; 25 | 26 | // The rest of these apply only if the value is undefined 27 | if (value !== undefined) return; 28 | 29 | // At this point, if it's a normal, non-modifier object, then a missing value is an error 30 | if (!operator) return SimpleSchema.ErrorTypes.REQUIRED; 31 | 32 | // Everything beyond this point deals with modifier objects only 33 | 34 | // We can skip the required check for keys that are ancestors of those in $set or 35 | // $setOnInsert because they will be created by MongoDB while setting. 36 | const keysWithValueInSet = getKeysWithValueInObj(obj.$set, key); 37 | if (keysWithValueInSet.length) return; 38 | const keysWithValueInSetOnInsert = getKeysWithValueInObj(obj.$setOnInsert, key); 39 | if (keysWithValueInSetOnInsert.length) return; 40 | 41 | // In the case of $set and $setOnInsert, the value may be undefined here 42 | // but it is set in another operator. So check that first. 43 | const fieldInfo = this.field(key); 44 | if (fieldInfo.isSet && fieldInfo.value !== null) return; 45 | 46 | // Required if in an array or sub object 47 | if (isInArrayItemObject || isInSubObject) return SimpleSchema.ErrorTypes.REQUIRED; 48 | 49 | // If we've got this far with an undefined $set or $setOnInsert value, it's a required error. 50 | if (operator === '$set' || operator === '$setOnInsert') return SimpleSchema.ErrorTypes.REQUIRED; 51 | } 52 | -------------------------------------------------------------------------------- /lib/clean/setAutoValues.js: -------------------------------------------------------------------------------- 1 | import getPositionsForAutoValue from './getPositionsForAutoValue'; 2 | import AutoValueRunner from './AutoValueRunner'; 3 | 4 | /** 5 | * @method sortAutoValueFunctions 6 | * @private 7 | * @param {Array} autoValueFunctions - Array of objects to be sorted 8 | * @returns {Array} Sorted array 9 | * 10 | * Stable sort of the autoValueFunctions (preserves order at the same field depth). 11 | */ 12 | export function sortAutoValueFunctions(autoValueFunctions) { 13 | const defaultFieldOrder = autoValueFunctions.reduce((acc, { fieldName }, index) => { 14 | acc[fieldName] = index; 15 | return acc; 16 | }, {}); 17 | 18 | // Sort by how many dots each field name has, asc, such that we can auto-create 19 | // objects and arrays before we run the autoValues for properties within them. 20 | // Fields of the same level (same number of dots) preserve should order from the original array. 21 | return autoValueFunctions.sort((a, b) => { 22 | const depthDiff = a.fieldName.split('.').length - b.fieldName.split('.').length; 23 | return depthDiff === 0 ? defaultFieldOrder[a.fieldName] - defaultFieldOrder[b.fieldName] : depthDiff; 24 | }); 25 | } 26 | 27 | /** 28 | * @method setAutoValues 29 | * @private 30 | * @param {Array} autoValueFunctions - An array of objects with func, fieldName, and closestSubschemaFieldName props 31 | * @param {MongoObject} mongoObject 32 | * @param {Boolean} [isModifier=false] - Is it a modifier doc? 33 | * @param {Object} [extendedAutoValueContext] - Object that will be added to the context when calling each autoValue function 34 | * @returns {undefined} 35 | * 36 | * Updates doc with automatic values from autoValue functions or default 37 | * values from defaultValue. Modifies the referenced object in place. 38 | */ 39 | function setAutoValues(autoValueFunctions, mongoObject, isModifier, isUpsert, extendedAutoValueContext) { 40 | const sortedAutoValueFunctions = sortAutoValueFunctions(autoValueFunctions); 41 | 42 | sortedAutoValueFunctions.forEach(({ func, fieldName, closestSubschemaFieldName }) => { 43 | const avRunner = new AutoValueRunner({ 44 | closestSubschemaFieldName, 45 | extendedAutoValueContext, 46 | func, 47 | isModifier, 48 | isUpsert, 49 | mongoObject, 50 | }); 51 | 52 | const positions = getPositionsForAutoValue({ fieldName, isModifier, mongoObject }); 53 | 54 | // Run the autoValue function once for each place in the object that 55 | // has a value or that potentially should. 56 | positions.forEach(avRunner.runForPosition.bind(avRunner)); 57 | }); 58 | } 59 | 60 | export default setAutoValues; 61 | -------------------------------------------------------------------------------- /lib/reactivity.tests.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { SimpleSchema } from './SimpleSchema'; 3 | import { Tracker } from 'meteor/tracker'; 4 | 5 | it('Tracker reactivity works for labels', function (done) { 6 | const schema = new SimpleSchema({ 7 | foo: { 8 | type: String, 9 | label: 'First', 10 | }, 11 | }, { tracker: Tracker }); 12 | 13 | const labelResults = []; 14 | function checkResults() { 15 | expect(labelResults).to.deep.equal([ 16 | 'First', 17 | 'Second', 18 | ]); 19 | done(); 20 | } 21 | 22 | Tracker.autorun(function autorun() { 23 | labelResults.push(schema.label('foo')); 24 | if (labelResults.length === 2) checkResults(); 25 | }); 26 | 27 | schema.labels({ foo: 'Second' }); 28 | }); 29 | 30 | it('Tracker reactivity works for labels in subschemas', function (done) { 31 | const subSchema = new SimpleSchema({ 32 | foo: { 33 | type: String, 34 | label: 'First', 35 | }, 36 | }); 37 | 38 | const schema = new SimpleSchema({ 39 | sub: subSchema, 40 | }, { tracker: Tracker }); 41 | 42 | const labelResults = []; 43 | function checkResults() { 44 | expect(labelResults).to.deep.equal([ 45 | 'First', 46 | 'Second', 47 | ]); 48 | done(); 49 | } 50 | 51 | Tracker.autorun(function autorun() { 52 | labelResults.push(schema.label('sub.foo')); 53 | if (labelResults.length === 2) checkResults(); 54 | }); 55 | 56 | subSchema.labels({ foo: 'Second' }); 57 | }); 58 | 59 | it('Tracker reactivity works for error messages', function (done) { 60 | const schema = new SimpleSchema({ 61 | foo: { 62 | type: String, 63 | label: 'First', 64 | }, 65 | }, { tracker: Tracker }); 66 | 67 | const errorResults = []; 68 | function checkResults() { 69 | expect(errorResults).to.deep.equal([ 70 | [], 71 | [ 72 | { 73 | name: 'foo', 74 | type: 'required', 75 | value: undefined 76 | } 77 | ], 78 | ]); 79 | done(); 80 | } 81 | 82 | Tracker.autorun(function autorun() { 83 | errorResults.push(schema.namedContext().validationErrors()); 84 | if (errorResults.length === 2) checkResults(); 85 | }); 86 | 87 | schema.namedContext().validate({}); 88 | }); 89 | 90 | it('Tracker reactivity works for error messages in subschemas', function (done) { 91 | const subSchema = new SimpleSchema({ 92 | foo: { 93 | type: String, 94 | label: 'First', 95 | }, 96 | }); 97 | 98 | const schema = new SimpleSchema({ 99 | sub: subSchema, 100 | }, { tracker: Tracker }); 101 | 102 | const errorResults = []; 103 | function checkResults() { 104 | expect(errorResults).to.deep.equal([ 105 | [], 106 | [ 107 | { 108 | name: 'sub', 109 | type: 'required', 110 | value: undefined 111 | }, 112 | { 113 | name: 'sub.foo', 114 | type: 'required', 115 | value: undefined 116 | } 117 | ], 118 | ]); 119 | done(); 120 | } 121 | 122 | Tracker.autorun(function autorun() { 123 | errorResults.push(schema.namedContext().validationErrors()); 124 | if (errorResults.length === 2) checkResults(); 125 | }); 126 | 127 | schema.namedContext().validate({}); 128 | }); 129 | -------------------------------------------------------------------------------- /lib/SimpleSchema_getQuickTypeForKey.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | describe('getQuickTypeForKey', function () { 8 | it('string', function () { 9 | const schema = new SimpleSchema({ 10 | foo: String, 11 | }); 12 | 13 | const type = schema.getQuickTypeForKey('foo'); 14 | expect(type).to.equal('string'); 15 | }); 16 | 17 | it('number', function () { 18 | const schema = new SimpleSchema({ 19 | foo: Number, 20 | }); 21 | 22 | const type = schema.getQuickTypeForKey('foo'); 23 | expect(type).to.equal('number'); 24 | }); 25 | 26 | it('int', function () { 27 | const schema = new SimpleSchema({ 28 | foo: SimpleSchema.Integer, 29 | }); 30 | 31 | const type = schema.getQuickTypeForKey('foo'); 32 | expect(type).to.equal('number'); 33 | }); 34 | 35 | it('boolean', function () { 36 | const schema = new SimpleSchema({ 37 | foo: Boolean, 38 | }); 39 | 40 | const type = schema.getQuickTypeForKey('foo'); 41 | expect(type).to.equal('boolean'); 42 | }); 43 | 44 | it('date', function () { 45 | const schema = new SimpleSchema({ 46 | foo: Date, 47 | }); 48 | 49 | const type = schema.getQuickTypeForKey('foo'); 50 | expect(type).to.equal('date'); 51 | }); 52 | 53 | it('object', function () { 54 | const schema = new SimpleSchema({ 55 | foo: Object, 56 | }); 57 | 58 | const type = schema.getQuickTypeForKey('foo'); 59 | expect(type).to.equal('object'); 60 | }); 61 | 62 | it('stringArray', function () { 63 | const schema = new SimpleSchema({ 64 | foo: [String], 65 | }); 66 | 67 | const type = schema.getQuickTypeForKey('foo'); 68 | expect(type).to.equal('stringArray'); 69 | }); 70 | 71 | it('numberArray', function () { 72 | const schema = new SimpleSchema({ 73 | foo: [Number], 74 | }); 75 | 76 | const type = schema.getQuickTypeForKey('foo'); 77 | expect(type).to.equal('numberArray'); 78 | }); 79 | 80 | it('int array', function () { 81 | const schema = new SimpleSchema({ 82 | foo: [SimpleSchema.Integer], 83 | }); 84 | 85 | const type = schema.getQuickTypeForKey('foo'); 86 | expect(type).to.equal('numberArray'); 87 | }); 88 | 89 | it('booleanArray', function () { 90 | const schema = new SimpleSchema({ 91 | foo: [Boolean], 92 | }); 93 | 94 | const type = schema.getQuickTypeForKey('foo'); 95 | expect(type).to.equal('booleanArray'); 96 | }); 97 | 98 | it('dateArray', function () { 99 | const schema = new SimpleSchema({ 100 | foo: [Date], 101 | }); 102 | 103 | const type = schema.getQuickTypeForKey('foo'); 104 | expect(type).to.equal('dateArray'); 105 | }); 106 | 107 | it('objectArray', function () { 108 | const schema = new SimpleSchema({ 109 | foo: [Object], 110 | }); 111 | 112 | const type = schema.getQuickTypeForKey('foo'); 113 | expect(type).to.equal('objectArray'); 114 | }); 115 | 116 | it('objectArray (subschema)', function () { 117 | const subschema = new SimpleSchema({ 118 | bar: String, 119 | }); 120 | 121 | const schema = new SimpleSchema({ 122 | foo: [subschema], 123 | }); 124 | 125 | const type = schema.getQuickTypeForKey('foo'); 126 | expect(type).to.equal('objectArray'); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /lib/clean/AutoValueRunner.js: -------------------------------------------------------------------------------- 1 | import clone from 'clone'; 2 | import { getParentOfKey } from '../utility'; 3 | 4 | function getFieldInfo(mongoObject, key) { 5 | const keyInfo = mongoObject.getInfoForKey(key) || {}; 6 | return { 7 | isSet: (keyInfo.value !== undefined), 8 | value: keyInfo.value, 9 | operator: keyInfo.operator || null, 10 | }; 11 | } 12 | 13 | export default class AutoValueRunner { 14 | constructor(options) { 15 | this.options = options; 16 | this.doneKeys = []; 17 | } 18 | 19 | runForPosition({ 20 | key: affectedKey, 21 | operator, 22 | position, 23 | value, 24 | }) { 25 | const { 26 | closestSubschemaFieldName, 27 | extendedAutoValueContext, 28 | func, 29 | isModifier, 30 | isUpsert, 31 | mongoObject, 32 | } = this.options; 33 | 34 | // If already called for this key, skip it 35 | if (this.doneKeys.includes(affectedKey)) return; 36 | 37 | const fieldParentName = getParentOfKey(affectedKey, true); 38 | const parentFieldInfo = getFieldInfo(mongoObject, fieldParentName.slice(0, -1)); 39 | 40 | let doUnset = false; 41 | 42 | if (Array.isArray(parentFieldInfo.value)) { 43 | if (isNaN(affectedKey.split('.').slice(-1).pop())) { 44 | // parent is an array, but the key to be set is not an integer (see issue #80) 45 | return; 46 | } 47 | } 48 | 49 | const autoValue = func.call({ 50 | closestSubschemaFieldName: closestSubschemaFieldName.length ? closestSubschemaFieldName : null, 51 | field(fName) { 52 | return getFieldInfo(mongoObject, closestSubschemaFieldName + fName); 53 | }, 54 | isModifier, 55 | isUpsert, 56 | isSet: (value !== undefined), 57 | key: affectedKey, 58 | operator, 59 | parentField() { 60 | return parentFieldInfo; 61 | }, 62 | siblingField(fName) { 63 | return getFieldInfo(mongoObject, fieldParentName + fName); 64 | }, 65 | unset() { 66 | doUnset = true; 67 | }, 68 | value, 69 | ...(extendedAutoValueContext || {}), 70 | }, mongoObject.getObject()); 71 | 72 | // Update tracking of which keys we've run autovalue for 73 | this.doneKeys.push(affectedKey); 74 | 75 | if (doUnset && position) mongoObject.removeValueForPosition(position); 76 | 77 | if (autoValue === undefined) return; 78 | 79 | // If the user's auto value is of the pseudo-modifier format, parse it 80 | // into operator and value. 81 | if (isModifier) { 82 | let op; 83 | let newValue; 84 | if (autoValue && typeof autoValue === 'object') { 85 | const avOperator = Object.keys(autoValue).find((avProp) => avProp.substring(0, 1) === '$'); 86 | if (avOperator) { 87 | op = avOperator; 88 | newValue = autoValue[avOperator]; 89 | } 90 | } 91 | 92 | // Add $set for updates and upserts if necessary. Keep this 93 | // above the "if (op)" block below since we might change op 94 | // in this line. 95 | if (!op && position.slice(0, 1) !== '$') { 96 | op = '$set'; 97 | newValue = autoValue; 98 | } 99 | 100 | if (op) { 101 | // Update/change value 102 | mongoObject.removeValueForPosition(position); 103 | mongoObject.setValueForPosition(`${op}[${affectedKey}]`, clone(newValue)); 104 | return; 105 | } 106 | } 107 | 108 | // Update/change value. Cloning is necessary in case it's an object, because 109 | // if we later set some keys within it, they'd be set on the original object, too. 110 | mongoObject.setValueForPosition(position, clone(autoValue)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/SimpleSchema_getObjectSchema.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | describe('getObjectSchema', function () { 8 | it('top level object', function () { 9 | const schema = new SimpleSchema({ 10 | foo: Object, 11 | 'foo.aaa': { 12 | type: String, 13 | optional: true, 14 | }, 15 | 'foo.bbb': { 16 | type: Object, 17 | optional: true, 18 | }, 19 | 'foo.bbb.zzz': { 20 | type: Number, 21 | }, 22 | }); 23 | 24 | const newSchema = schema.getObjectSchema('foo'); 25 | expect(newSchema.mergedSchema()).to.deep.equal({ 26 | aaa: { 27 | type: SimpleSchema.oneOf(String), 28 | label: 'Aaa', 29 | optional: true, 30 | }, 31 | bbb: { 32 | type: SimpleSchema.oneOf(Object), 33 | label: 'Bbb', 34 | optional: true, 35 | }, 36 | 'bbb.zzz': { 37 | type: SimpleSchema.oneOf(Number), 38 | label: 'Zzz', 39 | optional: false, 40 | }, 41 | }); 42 | }); 43 | 44 | it('second level object', function () { 45 | const schema = new SimpleSchema({ 46 | foo: Object, 47 | 'foo.aaa': { 48 | type: String, 49 | optional: true, 50 | }, 51 | 'foo.bbb': { 52 | type: Object, 53 | optional: true, 54 | }, 55 | 'foo.bbb.zzz': { 56 | type: Number, 57 | }, 58 | }); 59 | 60 | const newSchema = schema.getObjectSchema('foo.bbb'); 61 | expect(newSchema.mergedSchema()).to.deep.equal({ 62 | zzz: { 63 | type: SimpleSchema.oneOf(Number), 64 | label: 'Zzz', 65 | optional: false, 66 | }, 67 | }); 68 | }); 69 | 70 | it('object in array', function () { 71 | const schema = new SimpleSchema({ 72 | foo: Object, 73 | 'foo.aaa': { 74 | type: Array, 75 | optional: true, 76 | }, 77 | 'foo.aaa.$': { 78 | type: Object, 79 | }, 80 | 'foo.aaa.$.zzz': { 81 | type: String, 82 | optional: true, 83 | }, 84 | 'foo.aaa.$.yyy': { 85 | type: Boolean, 86 | }, 87 | }); 88 | 89 | const newSchema = schema.getObjectSchema('foo.aaa.$'); 90 | expect(newSchema.mergedSchema()).to.deep.equal({ 91 | zzz: { 92 | type: SimpleSchema.oneOf(String), 93 | label: 'Zzz', 94 | optional: true, 95 | }, 96 | yyy: { 97 | type: SimpleSchema.oneOf(Boolean), 98 | label: 'Yyy', 99 | optional: false, 100 | }, 101 | }); 102 | }); 103 | 104 | it('subschema', function () { 105 | const schema = new SimpleSchema({ 106 | foo: new SimpleSchema({ 107 | aaa: { 108 | type: String, 109 | optional: true, 110 | }, 111 | bbb: { 112 | type: Object, 113 | optional: true, 114 | }, 115 | 'bbb.zzz': { 116 | type: Number, 117 | }, 118 | }), 119 | }); 120 | 121 | const newSchema = schema.getObjectSchema('foo'); 122 | expect(newSchema.mergedSchema()).to.deep.equal({ 123 | aaa: { 124 | type: SimpleSchema.oneOf(String), 125 | label: 'Aaa', 126 | optional: true, 127 | }, 128 | bbb: { 129 | type: SimpleSchema.oneOf(Object), 130 | label: 'Bbb', 131 | optional: true, 132 | }, 133 | 'bbb.zzz': { 134 | type: SimpleSchema.oneOf(Number), 135 | label: 'Zzz', 136 | optional: false, 137 | }, 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /lib/clean/getPositionsForAutoValue.js: -------------------------------------------------------------------------------- 1 | import MongoObject from 'mongo-object'; 2 | import { getLastPartOfKey, getParentOfKey } from '../utility'; 3 | 4 | /** 5 | * A position is a place in the object where this field exists. 6 | * If no arrays are involved, then every field/key has at most 1 position. 7 | * If arrays are involved, then a field could have potentially unlimited positions. 8 | * 9 | * For example, the key 'a.b.$.c` would have these positions: 10 | * `a[b][0][c]` 11 | * `a[b][1][c]` 12 | * `a[b][2][c]` 13 | * 14 | * For this object: 15 | * { 16 | * a: { 17 | * b: [ 18 | * { c: 1 }, 19 | * { c: 1 }, 20 | * { c: 1 }, 21 | * ], 22 | * }, 23 | * } 24 | * 25 | * To make matters more complicated, we want to include not only the existing positions 26 | * but also the positions that might exist due to their parent object existing or their 27 | * parent object being auto-created by a MongoDB modifier that implies it. 28 | */ 29 | export default function getPositionsForAutoValue({ fieldName, isModifier, mongoObject }) { 30 | // Positions for this field 31 | const positions = mongoObject.getPositionsInfoForGenericKey(fieldName); 32 | 33 | // If the field is an object and will be created by MongoDB, 34 | // we don't need (and can't have) a value for it 35 | if (isModifier && mongoObject.getPositionsThatCreateGenericKey(fieldName).length > 0) { 36 | return positions; 37 | } 38 | 39 | // For simple top-level fields, just add an undefined would-be position 40 | // if there isn't a real position. 41 | if (fieldName.indexOf('.') === -1 && positions.length === 0) { 42 | positions.push({ 43 | key: fieldName, 44 | value: undefined, 45 | operator: isModifier ? '$set' : null, 46 | position: isModifier ? `$set[${fieldName}]` : fieldName, 47 | }); 48 | return positions; 49 | } 50 | 51 | const parentPath = getParentOfKey(fieldName); 52 | const lastPart = getLastPartOfKey(fieldName, parentPath); 53 | const lastPartWithBraces = lastPart.replace(/\./g, ']['); 54 | const parentPositions = mongoObject.getPositionsInfoForGenericKey(parentPath); 55 | 56 | if (parentPositions.length) { 57 | parentPositions.forEach((info) => { 58 | const childPosition = `${info.position}[${lastPartWithBraces}]`; 59 | if (!positions.find((i) => i.position === childPosition)) { 60 | positions.push({ 61 | key: `${info.key}.${lastPart}`, 62 | value: undefined, 63 | operator: info.operator, 64 | position: childPosition, 65 | }); 66 | } 67 | }); 68 | } else if (parentPath.slice(-2) !== '.$') { 69 | // positions that will create parentPath 70 | mongoObject.getPositionsThatCreateGenericKey(parentPath).forEach((info) => { 71 | const { operator, position } = info; 72 | let wouldBePosition; 73 | if (operator) { 74 | const next = position.slice(position.indexOf('[') + 1, position.indexOf(']')); 75 | const nextPieces = next.split('.'); 76 | 77 | const newPieces = []; 78 | let newKey; 79 | while (nextPieces.length && newKey !== parentPath) { 80 | newPieces.push(nextPieces.shift()); 81 | newKey = newPieces.join('.'); 82 | } 83 | newKey = `${newKey}.${fieldName.slice(newKey.length + 1)}`; 84 | wouldBePosition = `$set[${newKey}]`; 85 | } else { 86 | const lastPart2 = getLastPartOfKey(fieldName, parentPath); 87 | const lastPartWithBraces2 = lastPart2.replace(/\./g, ']['); 88 | wouldBePosition = `${position.slice(0, position.lastIndexOf('['))}[${lastPartWithBraces2}]`; 89 | } 90 | if (!positions.find((i) => i.position === wouldBePosition)) { 91 | positions.push({ 92 | key: MongoObject._positionToKey(wouldBePosition), 93 | value: undefined, 94 | operator: operator ? '$set' : null, 95 | position: wouldBePosition, 96 | }); 97 | } 98 | }); 99 | } 100 | 101 | return positions; 102 | } 103 | -------------------------------------------------------------------------------- /lib/SimpleSchema_autoValueFunctions.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema - autoValueFunctions', function () { 7 | it('simple', function () { 8 | const schema = new SimpleSchema({ 9 | a: { 10 | type: String, 11 | autoValue() {}, 12 | }, 13 | }); 14 | 15 | const autoValueFunctions = schema.autoValueFunctions(); 16 | expect(autoValueFunctions.length).to.equal(1); 17 | expect(!!autoValueFunctions[0].func).to.equal(true); 18 | expect(autoValueFunctions[0].fieldName).to.equal('a'); 19 | expect(autoValueFunctions[0].closestSubschemaFieldName).to.equal(''); 20 | }); 21 | 22 | it('one level of subschema', function () { 23 | const subschema = new SimpleSchema({ 24 | z: { 25 | type: Object, 26 | autoValue() {}, 27 | }, 28 | }); 29 | 30 | const schema = new SimpleSchema({ 31 | a: { 32 | type: Object, 33 | autoValue() {}, 34 | }, 35 | 'a.b': { 36 | type: String, 37 | autoValue() {}, 38 | }, 39 | c: { 40 | type: subschema, 41 | }, 42 | }); 43 | 44 | const autoValueFunctions = schema.autoValueFunctions(); 45 | expect(autoValueFunctions.length).to.equal(3); 46 | 47 | expect(!!autoValueFunctions[0].func).to.equal(true); 48 | expect(autoValueFunctions[0].fieldName).to.equal('a'); 49 | expect(autoValueFunctions[0].closestSubschemaFieldName).to.equal(''); 50 | 51 | expect(!!autoValueFunctions[1].func).to.equal(true); 52 | expect(autoValueFunctions[1].fieldName).to.equal('a.b'); 53 | expect(autoValueFunctions[1].closestSubschemaFieldName).to.equal(''); 54 | 55 | expect(!!autoValueFunctions[2].func).to.equal(true); 56 | expect(autoValueFunctions[2].fieldName).to.equal('c.z'); 57 | expect(autoValueFunctions[2].closestSubschemaFieldName).to.equal('c'); 58 | }); 59 | 60 | it('two levels of subschemas', function () { 61 | const subschema1 = new SimpleSchema({ 62 | x: { 63 | type: Object, 64 | autoValue() {}, 65 | }, 66 | 'x.m': { 67 | type: Array, 68 | autoValue() {}, 69 | }, 70 | 'x.m.$': { 71 | type: String, 72 | }, 73 | }); 74 | 75 | const subschema2 = new SimpleSchema({ 76 | z: { 77 | type: Object, 78 | autoValue() {}, 79 | }, 80 | 'z.y': { 81 | type: subschema1, 82 | }, 83 | }); 84 | 85 | const schema = new SimpleSchema({ 86 | a: { 87 | type: Object, 88 | autoValue() {}, 89 | }, 90 | 'a.b': { 91 | type: String, 92 | autoValue() {}, 93 | }, 94 | c: { 95 | type: subschema2, 96 | }, 97 | }); 98 | 99 | const autoValueFunctions = schema.autoValueFunctions(); 100 | expect(autoValueFunctions.length).to.equal(5); 101 | 102 | expect(!!autoValueFunctions[0].func).to.equal(true); 103 | expect(autoValueFunctions[0].fieldName).to.equal('a'); 104 | expect(autoValueFunctions[0].closestSubschemaFieldName).to.equal(''); 105 | 106 | expect(!!autoValueFunctions[1].func).to.equal(true); 107 | expect(autoValueFunctions[1].fieldName).to.equal('a.b'); 108 | expect(autoValueFunctions[1].closestSubschemaFieldName).to.equal(''); 109 | 110 | expect(!!autoValueFunctions[2].func).to.equal(true); 111 | expect(autoValueFunctions[2].fieldName).to.equal('c.z'); 112 | expect(autoValueFunctions[2].closestSubschemaFieldName).to.equal('c'); 113 | 114 | expect(!!autoValueFunctions[3].func).to.equal(true); 115 | expect(autoValueFunctions[3].fieldName).to.equal('c.z.y.x'); 116 | expect(autoValueFunctions[3].closestSubschemaFieldName).to.equal('c.z.y'); 117 | 118 | expect(!!autoValueFunctions[4].func).to.equal(true); 119 | expect(autoValueFunctions[4].fieldName).to.equal('c.z.y.x.m'); 120 | expect(autoValueFunctions[4].closestSubschemaFieldName).to.equal('c.z.y'); 121 | }); 122 | 123 | it('array of objects', function () { 124 | const subschema = new SimpleSchema({ 125 | z: { 126 | type: String, 127 | autoValue() {}, 128 | }, 129 | }); 130 | 131 | const schema = new SimpleSchema({ 132 | a: { 133 | type: Object, 134 | autoValue() {}, 135 | }, 136 | 'a.b': { 137 | type: Array, 138 | }, 139 | 'a.b.$': { 140 | type: subschema, 141 | }, 142 | }); 143 | 144 | const autoValueFunctions = schema.autoValueFunctions(); 145 | expect(autoValueFunctions.length).to.equal(2); 146 | 147 | expect(!!autoValueFunctions[0].func).to.equal(true); 148 | expect(autoValueFunctions[0].fieldName).to.equal('a'); 149 | expect(autoValueFunctions[0].closestSubschemaFieldName).to.equal(''); 150 | 151 | expect(!!autoValueFunctions[1].func).to.equal(true); 152 | expect(autoValueFunctions[1].fieldName).to.equal('a.b.$.z'); 153 | expect(autoValueFunctions[1].closestSubschemaFieldName).to.equal('a.b.$'); 154 | }); 155 | 156 | it('async functions'); 157 | }); 158 | -------------------------------------------------------------------------------- /lib/SimpleSchema_oneOf.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | describe('oneOf', function () { 8 | it('allows either type', function () { 9 | const schema = new SimpleSchema({ 10 | foo: SimpleSchema.oneOf(String, Number, Date), 11 | }); 12 | 13 | const test1 = { foo: 1 }; 14 | expect(function test1func () { 15 | schema.validate(test1); 16 | }).not.to.throw(); 17 | expect(typeof test1.foo).to.equal('number'); 18 | 19 | const test2 = { foo: 'bar' }; 20 | expect(function test2func () { 21 | schema.validate(test2); 22 | }).not.to.throw(); 23 | expect(typeof test2.foo).to.equal('string'); 24 | 25 | const test3 = { foo: new Date() }; 26 | expect(function test2func () { 27 | schema.validate(test3); 28 | }).not.to.throw(); 29 | expect(test3.foo instanceof Date).to.equal(true); 30 | 31 | const test4 = { foo: false }; 32 | expect(function test3func () { 33 | schema.validate(test4); 34 | }).to.throw(); 35 | expect(typeof test4.foo).to.equal('boolean'); 36 | }); 37 | 38 | it.skip('allows either type including schemas', function () { 39 | const schemaOne = new SimpleSchema({ 40 | itemRef: String, 41 | partNo: String, 42 | }); 43 | 44 | const schemaTwo = new SimpleSchema({ 45 | anotherIdentifier: String, 46 | partNo: String, 47 | }); 48 | 49 | const combinedSchema = new SimpleSchema({ 50 | item: SimpleSchema.oneOf(String, schemaOne, schemaTwo), 51 | }); 52 | 53 | let isValid = combinedSchema.namedContext().validate({ 54 | item: 'foo', 55 | }); 56 | expect(isValid).to.equal(true); 57 | 58 | isValid = combinedSchema.namedContext().validate({ 59 | item: { 60 | anotherIdentifier: 'hhh', 61 | partNo: 'ttt', 62 | }, 63 | }); 64 | expect(isValid).to.equal(true); 65 | 66 | isValid = combinedSchema.namedContext().validate({ 67 | item: { 68 | itemRef: 'hhh', 69 | partNo: 'ttt', 70 | }, 71 | }); 72 | expect(isValid).to.equal(true); 73 | }); 74 | 75 | it('is valid as long as one min value is met', function () { 76 | const schema = new SimpleSchema({ 77 | foo: SimpleSchema.oneOf({ 78 | type: SimpleSchema.Integer, 79 | min: 5, 80 | }, { 81 | type: SimpleSchema.Integer, 82 | min: 10, 83 | }), 84 | }); 85 | 86 | expect(function () { 87 | schema.validate({ foo: 7 }); 88 | }).not.to.throw(); 89 | }); 90 | 91 | it('works when one is an array', function () { 92 | const schema = new SimpleSchema({ 93 | foo: SimpleSchema.oneOf(String, Array), 94 | 'foo.$': String, 95 | }); 96 | 97 | expect(function () { 98 | schema.validate({ 99 | foo: 'bar', 100 | }); 101 | }).not.to.throw(); 102 | 103 | expect(function () { 104 | schema.validate({ 105 | foo: 1, 106 | }); 107 | }).to.throw(); 108 | 109 | expect(function () { 110 | schema.validate({ 111 | foo: [], 112 | }); 113 | }).not.to.throw(); 114 | 115 | expect(function () { 116 | schema.validate({ 117 | foo: ['bar', 'bin'], 118 | }); 119 | }).not.to.throw(); 120 | 121 | expect(function () { 122 | schema.validate({ 123 | foo: ['bar', 1], 124 | }); 125 | }).to.throw(); 126 | }); 127 | 128 | it('works when one is a schema', function () { 129 | const objSchema = new SimpleSchema({ 130 | _id: String, 131 | }); 132 | 133 | const schema = new SimpleSchema({ 134 | foo: SimpleSchema.oneOf(String, objSchema), 135 | }); 136 | 137 | expect(function () { 138 | schema.validate({ 139 | foo: 'bar', 140 | }); 141 | }).not.to.throw(); 142 | 143 | expect(function () { 144 | schema.validate({ 145 | foo: 1, 146 | }); 147 | }).to.throw(); 148 | 149 | expect(function () { 150 | schema.validate({ 151 | foo: [], 152 | }); 153 | }).to.throw(); 154 | 155 | expect(function () { 156 | schema.validate({ 157 | foo: {}, 158 | }); 159 | }).to.throw(); 160 | 161 | expect(function () { 162 | schema.validate({ 163 | foo: { 164 | _id: 'ID', 165 | }, 166 | }); 167 | }).not.to.throw(); 168 | }); 169 | 170 | it('is invalid if neither min value is met', function () { 171 | const schema = new SimpleSchema({ 172 | foo: SimpleSchema.oneOf({ 173 | type: SimpleSchema.Integer, 174 | min: 5, 175 | }, { 176 | type: SimpleSchema.Integer, 177 | min: 10, 178 | }), 179 | }); 180 | 181 | expect(function () { 182 | schema.validate({ foo: 3 }); 183 | }).to.throw(); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /lib/testHelpers/testSchema.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from '../SimpleSchema'; 2 | import Address from './Address'; 3 | 4 | const refSchema = new SimpleSchema({ 5 | string: { 6 | type: String, 7 | optional: true, 8 | }, 9 | number: { 10 | type: Number, 11 | optional: true, 12 | }, 13 | }); 14 | 15 | const testSchema = new SimpleSchema({ 16 | string: { 17 | type: String, 18 | optional: true, 19 | }, 20 | minMaxString: { 21 | type: String, 22 | optional: true, 23 | min: 10, 24 | max: 20, 25 | regEx: /^[a-z0-9_]+$/, 26 | }, 27 | minMaxStringArray: { 28 | type: Array, 29 | optional: true, 30 | minCount: 1, 31 | maxCount: 2, 32 | }, 33 | 'minMaxStringArray.$': { 34 | type: String, 35 | min: 10, 36 | max: 20, 37 | }, 38 | allowedStrings: { 39 | type: String, 40 | optional: true, 41 | allowedValues: ['tuna', 'fish', 'salad'], 42 | }, 43 | allowedStringsArray: { 44 | type: Array, 45 | optional: true, 46 | }, 47 | 'allowedStringsArray.$': { 48 | type: String, 49 | allowedValues: ['tuna', 'fish', 'salad'], 50 | }, 51 | allowedStringsSet: { 52 | type: Array, 53 | optional: true, 54 | }, 55 | 'allowedStringsSet.$': { 56 | type: String, 57 | allowedValues: new Set(['tuna', 'fish', 'salad']), 58 | }, 59 | boolean: { 60 | type: Boolean, 61 | optional: true, 62 | }, 63 | booleanArray: { 64 | type: Array, 65 | optional: true, 66 | }, 67 | 'booleanArray.$': { 68 | type: Boolean, 69 | }, 70 | number: { 71 | type: SimpleSchema.Integer, 72 | optional: true, 73 | }, 74 | sub: { 75 | type: Object, 76 | optional: true, 77 | }, 78 | 'sub.number': { 79 | type: SimpleSchema.Integer, 80 | optional: true, 81 | }, 82 | minMaxNumber: { 83 | type: SimpleSchema.Integer, 84 | optional: true, 85 | min: 10, 86 | max: 20, 87 | }, 88 | minZero: { 89 | type: SimpleSchema.Integer, 90 | optional: true, 91 | min: 0, 92 | }, 93 | maxZero: { 94 | type: SimpleSchema.Integer, 95 | optional: true, 96 | max: 0, 97 | }, 98 | minMaxNumberCalculated: { 99 | type: SimpleSchema.Integer, 100 | optional: true, 101 | min() { 102 | return 10; 103 | }, 104 | max() { 105 | return 20; 106 | }, 107 | }, 108 | minMaxNumberExclusive: { 109 | type: SimpleSchema.Integer, 110 | optional: true, 111 | min: 10, 112 | max: 20, 113 | exclusiveMax: true, 114 | exclusiveMin: true, 115 | }, 116 | minMaxNumberInclusive: { 117 | type: SimpleSchema.Integer, 118 | optional: true, 119 | min: 10, 120 | max: 20, 121 | exclusiveMax: false, 122 | exclusiveMin: false, 123 | }, 124 | allowedNumbers: { 125 | type: SimpleSchema.Integer, 126 | optional: true, 127 | allowedValues: [1, 2, 3], 128 | }, 129 | allowedNumbersArray: { 130 | type: Array, 131 | optional: true, 132 | }, 133 | 'allowedNumbersArray.$': { 134 | type: SimpleSchema.Integer, 135 | allowedValues: [1, 2, 3], 136 | }, 137 | allowedNumbersSet: { 138 | type: Array, 139 | optional: true, 140 | }, 141 | 'allowedNumbersSet.$': { 142 | type: SimpleSchema.Integer, 143 | allowedValues: new Set([1, 2, 3]), 144 | }, 145 | decimal: { 146 | type: Number, 147 | optional: true, 148 | }, 149 | date: { 150 | type: Date, 151 | optional: true, 152 | }, 153 | dateArray: { 154 | type: Array, 155 | optional: true, 156 | }, 157 | 'dateArray.$': { 158 | type: Date, 159 | }, 160 | minMaxDate: { 161 | type: Date, 162 | optional: true, 163 | min: (new Date(Date.UTC(2013, 0, 1))), 164 | max: (new Date(Date.UTC(2013, 11, 31))), 165 | }, 166 | minMaxDateCalculated: { 167 | type: Date, 168 | optional: true, 169 | min() { 170 | return (new Date(Date.UTC(2013, 0, 1))); 171 | }, 172 | max() { 173 | return (new Date(Date.UTC(2013, 11, 31))); 174 | }, 175 | }, 176 | email: { 177 | type: String, 178 | regEx: SimpleSchema.RegEx.Email, 179 | optional: true, 180 | }, 181 | url: { 182 | type: String, 183 | optional: true, 184 | custom() { 185 | if (!this.isSet) return; 186 | try { 187 | new URL(this.value); // eslint-disable-line 188 | } catch (err) { 189 | return 'badUrl'; 190 | } 191 | }, 192 | }, 193 | customObject: { 194 | type: Address, 195 | optional: true, 196 | blackbox: true, 197 | }, 198 | blackBoxObject: { 199 | type: Object, 200 | optional: true, 201 | blackbox: true, 202 | }, 203 | objectArray: { 204 | type: Array, 205 | optional: true, 206 | }, 207 | 'objectArray.$': { 208 | type: Object, 209 | optional: true, 210 | }, 211 | refObject: { 212 | type: refSchema, 213 | optional: true, 214 | }, 215 | refSchemaArray: { 216 | type: Array, 217 | optional: true, 218 | }, 219 | 'refSchemaArray.$': { 220 | type: refSchema, 221 | optional: true, 222 | }, 223 | }); 224 | 225 | testSchema.messageBox.messages({ 226 | minCount: 'blah', 227 | 'regEx email': '[label] is not a valid email address', 228 | 'badUrl url': '[label] is not a valid URL', 229 | }); 230 | 231 | export default testSchema; 232 | -------------------------------------------------------------------------------- /lib/regExp.js: -------------------------------------------------------------------------------- 1 | // this domain regex matches all domains that have at least one . 2 | // sadly IPv4 Adresses will be caught too but technically those are valid domains 3 | // this expression is extracted from the original RFC 5322 mail expression 4 | // a modification enforces that the tld consists only of characters 5 | const rxDomain = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z](?:[a-z-]*[a-z])?'; 6 | // this domain regex matches everythign that could be a domain in intranet 7 | // that means "localhost" is a valid domain 8 | const rxNameDomain = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.|$))+'; 9 | // strict IPv4 expression which allows 0-255 per oktett 10 | const rxIPv4 = '(?:(?:[0-1]?\\d{1,2}|2[0-4]\\d|25[0-5])(?:\\.|$)){4}'; 11 | // strict IPv6 expression which allows (and validates) all shortcuts 12 | const rxIPv6 = '(?:(?:[\\dA-Fa-f]{1,4}(?::|$)){8}' // full adress 13 | + '|(?=(?:[^:\\s]|:[^:\\s])*::(?:[^:\\s]|:[^:\\s])*$)' // or min/max one '::' 14 | + '[\\dA-Fa-f]{0,4}(?:::?(?:[\\dA-Fa-f]{1,4}|$)){1,6})'; // and short adress 15 | // this allows domains (also localhost etc) and ip adresses 16 | const rxWeakDomain = `(?:${[rxNameDomain, rxIPv4, rxIPv6].join('|')})`; 17 | // unique id from the random package also used by minimongo 18 | // min and max are used to set length boundaries 19 | // set both for explicit lower and upper bounds 20 | // set min as integer and max to null for explicit lower bound and arbitrary upper bound 21 | // set none for arbitrary length 22 | // set only min for fixed length 23 | // character list: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L88 24 | // string length: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L143 25 | const isValidBound = (value, lower) => !value || (Number.isSafeInteger(value) && value > lower); 26 | const idOfLength = (min, max) => { 27 | if (!isValidBound(min, 0)) throw new Error(`Expected a non-negative safe integer, got ${min}`); 28 | if (!isValidBound(max, min)) throw new Error(`Expected a non-negative safe integer greater than 1 and greater than min, got ${max}`); 29 | let bounds; 30 | if (min && max) bounds = `${min},${max}`; 31 | else if (min && max === null) bounds = `${min},`; 32 | else if (min && !max) bounds = `${min}`; 33 | else if (!min && !max) bounds = '0,'; 34 | else throw new Error(`Unexpected state for min (${min}) and max (${max})`); 35 | return new RegExp(`^[23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz]{${bounds}}$`); 36 | }; 37 | 38 | const regEx = { 39 | // We use the RegExp suggested by W3C in http://www.w3.org/TR/html5/forms.html#valid-e-mail-address 40 | // This is probably the same logic used by most browsers when type=email, which is our goal. It is 41 | // a very permissive expression. Some apps may wish to be more strict and can write their own RegExp. 42 | Email: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, 43 | 44 | // Like Email but requires the TLD (.com, etc) 45 | EmailWithTLD: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 46 | 47 | Domain: new RegExp(`^${rxDomain}$`), 48 | WeakDomain: new RegExp(`^${rxWeakDomain}$`), 49 | 50 | IP: new RegExp(`^(?:${rxIPv4}|${rxIPv6})$`), 51 | IPv4: new RegExp(`^${rxIPv4}$`), 52 | IPv6: new RegExp(`^${rxIPv6}$`), 53 | // URL RegEx from https://gist.github.com/dperini/729294 54 | // DEPRECATED! Known 2nd degree polynomial ReDoS vulnerability. 55 | // Use a custom validator such as this to validate URLs: 56 | // custom() { 57 | // if (!this.isSet) return; 58 | // try { 59 | // new URL(this.value); 60 | // } catch (err) { 61 | // return 'badUrl'; 62 | // } 63 | // } 64 | // eslint-disable-next-line redos/no-vulnerable 65 | Url: /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i, 66 | // default id is defined with exact 17 chars of length 67 | Id: idOfLength(17), 68 | idOfLength, 69 | // allows for a 5 digit zip code followed by a whitespace or dash and then 4 more digits 70 | // matches 11111 and 11111-1111 and 11111 1111 71 | ZipCode: /^\d{5}(?:[-\s]\d{4})?$/, 72 | // taken from Google's libphonenumber library 73 | // https://github.com/googlei18n/libphonenumber/blob/master/javascript/i18n/phonenumbers/phonenumberutil.js 74 | // reference the VALID_PHONE_NUMBER_PATTERN key 75 | // allows for common phone number symbols including + () and - 76 | // DEPRECATED! Known 2nd degree polynomial ReDoS vulnerability. 77 | // Instead, use a custom validation function, with a high quality 78 | // phone number validation package that meets your needs. 79 | // eslint-disable-next-line redos/no-vulnerable 80 | Phone: /^[0-90-9٠-٩۰-۹]{2}$|^[++]*(?:[-x‐-―−ー--/ ­​⁠ ()()[].\[\]/~⁓∼~*]*[0-90-9٠-٩۰-۹]){3,}[-x‐-―−ー--/ ­​⁠ ()()[].\[\]/~⁓∼~*A-Za-z0-90-9٠-٩۰-۹]*(?:;ext=([0-90-9٠-٩۰-۹]{1,20})|[ \t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|доб|anexo)[:\..]?[ \t,-]*([0-90-9٠-٩۰-۹]{1,20})#?|[ \t,]*(?:[xx##~~]|int|int)[:\..]?[ \t,-]*([0-90-9٠-٩۰-۹]{1,9})#?|[- ]+([0-90-9٠-٩۰-۹]{1,6})#|[ \t]*(?:,{2}|;)[:\..]?[ \t,-]*([0-90-9٠-٩۰-۹]{1,15})#?|[ \t]*(?:,)+[:\..]?[ \t,-]*([0-90-9٠-٩۰-۹]{1,9})#?)?$/i, // eslint-disable-line no-irregular-whitespace 81 | }; 82 | 83 | export default regEx; 84 | -------------------------------------------------------------------------------- /lib/SimpleSchema_custom.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema, ValidationContext } from './SimpleSchema'; 5 | import expectErrorLength from './testHelpers/expectErrorLength'; 6 | import expectErrorOfTypeLength from './testHelpers/expectErrorOfTypeLength'; 7 | 8 | const schema = new SimpleSchema({ 9 | password: { 10 | type: String, 11 | }, 12 | confirmPassword: { 13 | type: String, 14 | custom() { 15 | if (this.value !== this.field('password').value) { 16 | return 'passwordMismatch'; 17 | } 18 | }, 19 | }, 20 | }); 21 | 22 | const requiredCustomSchema = new SimpleSchema({ 23 | a: { 24 | type: Array, 25 | custom() { 26 | // Just adding custom to trigger extra validation 27 | }, 28 | }, 29 | 'a.$': { 30 | type: Object, 31 | custom() { 32 | // Just adding custom to trigger extra validation 33 | }, 34 | }, 35 | b: { 36 | type: Array, 37 | custom() { 38 | // Just adding custom to trigger extra validation 39 | }, 40 | }, 41 | 'b.$': { 42 | type: Object, 43 | custom() { 44 | // Just adding custom to trigger extra validation 45 | }, 46 | }, 47 | }); 48 | 49 | describe('SimpleSchema - Validation Against Another Key', function () { 50 | describe('normal', function () { 51 | it('valid', function () { 52 | expectErrorLength(schema, { 53 | password: 'password', 54 | confirmPassword: 'password', 55 | }).to.deep.equal(0); 56 | }); 57 | 58 | it('invalid', function () { 59 | expectErrorOfTypeLength('passwordMismatch', schema, { 60 | password: 'password', 61 | confirmPassword: 'password1', 62 | }).to.deep.equal(1); 63 | }); 64 | }); 65 | 66 | describe('modifier with $setOnInsert', function () { 67 | it('valid', function () { 68 | expectErrorLength(schema, { 69 | $setOnInsert: { 70 | password: 'password', 71 | confirmPassword: 'password', 72 | }, 73 | }, { modifier: true, upsert: true }).to.deep.equal(0); 74 | }); 75 | 76 | it('invalid', function () { 77 | expectErrorOfTypeLength('passwordMismatch', schema, { 78 | $setOnInsert: { 79 | password: 'password', 80 | confirmPassword: 'password1', 81 | }, 82 | }, { modifier: true, upsert: true }).to.deep.equal(1); 83 | }); 84 | }); 85 | 86 | describe('modifier with $set', function () { 87 | it('valid', function () { 88 | expectErrorLength(schema, { 89 | $set: { 90 | password: 'password', 91 | confirmPassword: 'password', 92 | }, 93 | }, { modifier: true }).to.deep.equal(0); 94 | }); 95 | 96 | it('invalid', function () { 97 | expectErrorOfTypeLength('passwordMismatch', schema, { 98 | $set: { 99 | password: 'password', 100 | confirmPassword: 'password1', 101 | }, 102 | }, { modifier: true }).to.deep.equal(1); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('custom', function () { 108 | it('custom validator has this.validationContext set', function () { 109 | let ok = false; 110 | 111 | const customSchema = new SimpleSchema({ 112 | foo: { 113 | type: String, 114 | optional: true, 115 | custom() { 116 | if (this.validationContext instanceof ValidationContext) ok = true; 117 | }, 118 | }, 119 | }); 120 | 121 | customSchema.namedContext().validate({}); 122 | expect(ok).to.equal(true); 123 | }); 124 | 125 | it('custom validation runs even when the optional field is undefined', function () { 126 | const customSchema = new SimpleSchema({ 127 | foo: { 128 | type: String, 129 | optional: true, 130 | custom: () => 'custom', 131 | }, 132 | }); 133 | 134 | const context = customSchema.namedContext(); 135 | context.validate({}); 136 | expect(context.validationErrors().length).to.deep.equal(1); 137 | expect(context.validationErrors()[0]).to.deep.equal({ name: 'foo', type: 'custom', value: undefined }); 138 | }); 139 | 140 | it('custom validation runs when string is unset', function () { 141 | const customSchema = new SimpleSchema({ 142 | foo: { 143 | type: String, 144 | optional: true, 145 | custom: () => 'custom', 146 | }, 147 | }); 148 | 149 | const context = customSchema.namedContext(); 150 | context.validate({ 151 | $unset: { 152 | foo: '', 153 | }, 154 | }, { modifier: true }); 155 | expect(context.validationErrors().length).to.deep.equal(1); 156 | expect(context.validationErrors()[0]).to.deep.equal({ name: 'foo', type: 'custom', value: '' }); 157 | }); 158 | 159 | it('we do not get required errors for a required field that has a `custom` function when we are $setting', function () { 160 | const context = requiredCustomSchema.namedContext(); 161 | 162 | expect(context.validate({ 163 | $set: { 164 | a: [{}], 165 | }, 166 | }, { modifier: true })).to.deep.equal(true); 167 | 168 | expect(context.validate({ 169 | $set: { 170 | 'a.0': {}, 171 | }, 172 | }, { modifier: true })).to.deep.equal(true); 173 | }); 174 | 175 | it('we do not get required errors for a required field that has a `custom` function when we are $pushing', function () { 176 | const context = requiredCustomSchema.namedContext(); 177 | expect(context.validate({ 178 | $push: { 179 | a: {}, 180 | }, 181 | }, { modifier: true })).to.deep.equal(true); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to aldeed:simple-schema 2 | 3 | > Any contribution to this repository is highly appreciated! 4 | 5 | 6 | 7 | 8 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 9 | 10 | - [Introduction](#introduction) 11 | - [Setup development env](#setup-development-env) 12 | - [Clone project and create a new branch to work on](#clone-project-and-create-a-new-branch-to-work-on) 13 | - [Initialize test app](#initialize-test-app) 14 | - [Development toolchain](#development-toolchain) 15 | - [Linter](#linter) 16 | - [Tests](#tests) 17 | - [Once](#once) 18 | - [Watch](#watch) 19 | - [Coverage](#coverage) 20 | - [Open a pull request](#open-a-pull-request) 21 | - [Code review process](#code-review-process) 22 | - [Questions](#questions) 23 | - [Credits](#credits) 24 | 25 | 26 | 27 | ## Introduction 28 | 29 | First, thank you for considering contributing to simpl-schema! It's people like you that make the open source community such a great community! 😊 30 | 31 | We welcome any type of contribution, not only code. You can help with 32 | 33 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 34 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 35 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 36 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 37 | - **Money**: we [welcome financial contributions](https://github.com/https://github.com/Meteor-Community-Packages). 38 | 39 | ## Setup development env 40 | 41 | ### Clone project and create a new branch to work on 42 | 43 | First, clone this repository and create a new branch to work on. 44 | Branch names should follow the GitFlow standard and start with a descriptive prefix of their intended outcome, for example: 45 | 46 | - `feature/` for features 47 | - `fix/` for general fixes 48 | 49 | Then the name of the branch should describe the purpose of the branch and potentially reference the issue number it is solving. 50 | 51 | ```shell 52 | $ git clone git@github.com:Meteor-Community-Packages/meteor-simple-schema.git 53 | $ cd meteor-simple-schema 54 | $ git checkout -b fix/some-issue 55 | ``` 56 | 57 | ### Initialize test app 58 | 59 | We use a proxy Meteor application to run our tests and handle coverage etc. 60 | This app contains several npm scripts to provide the complete toolchain that is required 61 | for your development and testing needs. 62 | 63 | The setup is very easy. Go into the `tests` directory, install dependencies and link 64 | the package: 65 | 66 | ```shell 67 | $ cd tests 68 | $ meteor npm install 69 | $ meteor npm run setup # this is important for the tools to work! 70 | ``` 71 | 72 | ## Development toolchain 73 | 74 | The `tests` comes with some builtin scripts you can utilize during your development. 75 | They will also be picked up by our CI during pull requests. 76 | Therefore, it's a good call for you, that if they pass or fail, the CI will do so, too. 77 | 78 | **Note: all tools require the npm `setup` script has been executed at least once!** 79 | 80 | ### Linter 81 | 82 | We use `standard` as our linter. You can run either the linter or use it's autofix feature for 83 | the most common issues: 84 | 85 | ```shell 86 | # in tests 87 | $ meteor npm run lint # show only outputs 88 | $ meteor npm run lint:fix # with fixes + outputs 89 | ``` 90 | 91 | ### Tests 92 | 93 | We provide three forms of tests: once, watch, coverage 94 | 95 | #### Once 96 | 97 | Simply runs the test suite once, without coverage collection: 98 | 99 | ```shell 100 | $ meteor npm run test 101 | ``` 102 | 103 | #### Watch 104 | 105 | Runs the test suite in watch mode, good to use during active development, where your changes 106 | are picked up automatically to re-run the tests: 107 | 108 | ```shell 109 | $ meteor npm run test:watch 110 | ``` 111 | 112 | #### Coverage 113 | 114 | Runs the test suite once, including coverage report generation. 115 | Generates an html and json report output. 116 | 117 | ```shell 118 | $ meteor npm run test:coverage 119 | $ meteor npm run report # summary output in console 120 | ``` 121 | 122 | If you want to watch the HTML output to find (un)covered lines, open 123 | the file at `tests/.coverage/index.html` in your browser. 124 | 125 | ## Open a pull request 126 | 127 | If you open a pull request, please make sure the following requirements are met: 128 | 129 | - the `lint` script is passing 130 | - the `test` script is passing 131 | - your contribution is on point and solves one issue (not multiple) 132 | - your commit messages are descriptive and informative 133 | - complex changes are documented in the code with comments or jsDoc-compatible documentation 134 | 135 | Please understand, that there will be a review process and your contribution 136 | might require changes before being merged. This is entirely to ensure quality and is 137 | never used as a personal offense. 138 | 139 | 140 | ## Code review process 141 | 142 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in 143 | smaller chunks that are easier to review and merge. 144 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 145 | 146 | ## Questions 147 | 148 | If you have any questions, [create an issue](https://github.com/Meteor-Community-Packages/meteor-simple-schema/issues) 149 | (protip: do a quick search first to see if someone else didn't ask the same question before!). 150 | 151 | ## Credits 152 | 153 | Thank you to all the people who have already contributed to this project: 154 | 155 | 156 | -------------------------------------------------------------------------------- /lib/ValidationContext.js: -------------------------------------------------------------------------------- 1 | import MongoObject from 'mongo-object'; 2 | import doValidation from './doValidation'; 3 | 4 | /** 5 | * @typedef ValidationError 6 | * @type object 7 | * @property name {string} error name 8 | * @property type {string} error type name 9 | * @property value {string} actuall error message value 10 | */ 11 | 12 | /** 13 | * State representation of a validation for 14 | * a given schema. 15 | * 16 | * 17 | */ 18 | export default class ValidationContext { 19 | /** 20 | * @param {SimpleSchema} ss SimpleSchema instance to use for validation 21 | * @param {String} [name] Optional context name, accessible on context.name. 22 | */ 23 | constructor(ss, name) { 24 | this.name = name; 25 | 26 | this._simpleSchema = ss; 27 | this._schema = ss.schema(); 28 | this._schemaKeys = Object.keys(this._schema); 29 | this._validationErrors = []; 30 | this._deps = {}; 31 | 32 | // Set up validation dependencies 33 | const { tracker } = ss._constructorOptions; 34 | this.reactive(tracker); 35 | } 36 | //--------------------------------------------------------------------------- 37 | // PUBLIC 38 | //--------------------------------------------------------------------------- 39 | 40 | /** 41 | * Makes this validation context 42 | * reactive for Meteor-Tracker. 43 | * @param tracker {Tracker} 44 | */ 45 | reactive(tracker) { 46 | if (tracker && Object.keys(this._deps).length === 0) { 47 | this._depsAny = new tracker.Dependency(); 48 | this._schemaKeys.forEach((key) => { 49 | this._deps[key] = new tracker.Dependency(); 50 | }); 51 | } 52 | } 53 | 54 | /** 55 | * Merges existing with a list of new validation errors. 56 | * Reactive. 57 | * @param errors ValidationError[] 58 | */ 59 | setValidationErrors(errors) { 60 | const previousValidationErrors = this._validationErrors.map((o) => o.name); 61 | const newValidationErrors = errors.map((o) => o.name); 62 | 63 | this._validationErrors = errors; 64 | 65 | // Mark all previous plus all new as changed 66 | const changedKeys = previousValidationErrors.concat(newValidationErrors); 67 | this._markKeysChanged(changedKeys); 68 | } 69 | 70 | /** 71 | * Adds new validation errors to the list. 72 | * @param errors ValidationError[] 73 | */ 74 | addValidationErrors(errors) { 75 | const newValidationErrors = errors.map((o) => o.name); 76 | 77 | errors.forEach((error) => this._validationErrors.push(error)); 78 | 79 | // Mark all new as changed 80 | this._markKeysChanged(newValidationErrors); 81 | } 82 | 83 | /** 84 | * Flushes/empties the list of validation errors. 85 | */ 86 | reset() { 87 | this.setValidationErrors([]); 88 | } 89 | 90 | /** 91 | * Returns a validation error for a given key. 92 | * @param key {string} the key of the field to access errors for 93 | * @param genericKey {string} generic version of the key, you usually don't need 94 | * to explcitly call this. If you do, you need to wrap it using `MongoObject.makeKeyGeneric` 95 | * @return {ValidationError|undefined} 96 | */ 97 | getErrorForKey(key, genericKey = MongoObject.makeKeyGeneric(key)) { 98 | const errors = this._validationErrors; 99 | const errorForKey = errors.find((error) => error.name === key); 100 | if (errorForKey) return errorForKey; 101 | 102 | return errors.find((error) => error.name === genericKey); 103 | } 104 | 105 | /** 106 | * Returns, whether there is an error for a given key. Reactive. 107 | * @param key {string} 108 | * @param genericKey {string} 109 | * @return {boolean} 110 | */ 111 | keyIsInvalid(key, genericKey = MongoObject.makeKeyGeneric(key)) { 112 | if (Object.prototype.hasOwnProperty.call(this._deps, genericKey)) this._deps[genericKey].depend(); 113 | 114 | return this._keyIsInvalid(key, genericKey); 115 | } 116 | 117 | /** 118 | * 119 | * @param key 120 | * @param genericKey 121 | * @return {string|*} 122 | */ 123 | keyErrorMessage(key, genericKey = MongoObject.makeKeyGeneric(key)) { 124 | if (Object.prototype.hasOwnProperty.call(this._deps, genericKey)) this._deps[genericKey].depend(); 125 | 126 | const errorObj = this.getErrorForKey(key, genericKey); 127 | if (!errorObj) return ''; 128 | 129 | return this._simpleSchema.messageForError(errorObj); 130 | } 131 | 132 | /** 133 | * Validates the object against the simple schema 134 | * and sets a reactive array of error objects. 135 | * @param obj {object} the document (object) to validate 136 | * @param extendedCustomcontext {object=} 137 | * @param ignoreTypes {string[]=} list of names of ValidationError types to ignore 138 | * @param keysToValidate {string[]=} list of field names (keys) to validate. Other keys are ignored then 139 | * @param isModifier {boolean=} set to true if the document contains MongoDB modifiers 140 | * @param mongoObject {MongoObject=} MongoObject instance to generate keyInfo 141 | * @param isUpsert {boolean=} set to true if the document contains upsert modifiers 142 | * @return {boolean} true if no ValidationError was found, otherwise false 143 | */ 144 | validate(obj, { 145 | extendedCustomContext = {}, 146 | ignore: ignoreTypes = [], 147 | keys: keysToValidate, 148 | modifier: isModifier = false, 149 | mongoObject, 150 | upsert: isUpsert = false, 151 | } = {}) { 152 | const validationErrors = doValidation({ 153 | extendedCustomContext, 154 | ignoreTypes, 155 | isModifier, 156 | isUpsert, 157 | keysToValidate, 158 | mongoObject, 159 | obj, 160 | schema: this._simpleSchema, 161 | validationContext: this, 162 | }); 163 | 164 | if (keysToValidate) { 165 | // We have only revalidated the listed keys, so if there 166 | // are any other existing errors that are NOT in the keys list, 167 | // we should keep these errors. 168 | /* eslint-disable no-restricted-syntax */ 169 | for (const error of this._validationErrors) { 170 | const wasValidated = keysToValidate.some((key) => key === error.name || error.name.startsWith(`${key}.`)); 171 | if (!wasValidated) validationErrors.push(error); 172 | } 173 | /* eslint-enable no-restricted-syntax */ 174 | } 175 | 176 | this.setValidationErrors(validationErrors); 177 | 178 | // Return true if it was valid; otherwise, return false 179 | return !validationErrors.length; 180 | } 181 | 182 | /** 183 | * returns if this context has no errors. reactive. 184 | * @return {boolean} 185 | */ 186 | isValid() { 187 | this._depsAny && this._depsAny.depend(); 188 | return this._validationErrors.length === 0; 189 | } 190 | 191 | /** 192 | * returns the list of validation errors. reactive. 193 | * @return {ValidationError[]} 194 | */ 195 | validationErrors() { 196 | this._depsAny && this._depsAny.depend(); 197 | return this._validationErrors; 198 | } 199 | 200 | clean(...args) { 201 | return this._simpleSchema.clean(...args); 202 | } 203 | 204 | //--------------------------------------------------------------------------- 205 | // PRIVATE 206 | //--------------------------------------------------------------------------- 207 | 208 | _markKeyChanged(key) { 209 | const genericKey = MongoObject.makeKeyGeneric(key); 210 | if (Object.prototype.hasOwnProperty.call(this._deps, genericKey)) this._deps[genericKey].changed(); 211 | } 212 | 213 | _markKeysChanged(keys) { 214 | if (!keys || !Array.isArray(keys) || !keys.length) return; 215 | 216 | keys.forEach((key) => this._markKeyChanged(key)); 217 | 218 | this._depsAny && this._depsAny.changed(); 219 | } 220 | 221 | _keyIsInvalid(key, genericKey) { 222 | return !!this.getErrorForKey(key, genericKey); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/clean.js: -------------------------------------------------------------------------------- 1 | import clone from 'clone'; 2 | import MongoObject from 'mongo-object'; 3 | import { isEmptyObject, looksLikeModifier } from './utility'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | import convertToProperType from './clean/convertToProperType'; 6 | import setAutoValues from './clean/setAutoValues'; 7 | import typeValidator from './validation/typeValidator'; 8 | 9 | const operatorsToIgnoreValue = ['$unset', '$currentDate']; 10 | 11 | /** 12 | * @param {SimpleSchema} ss - A SimpleSchema instance 13 | * @param {Object} doc - Document or modifier to clean. Referenced object will be modified in place. 14 | * @param {Object} [options] 15 | * @param {Boolean} [options.mutate=false] - Mutate doc. Set this to true to improve performance if you don't mind mutating the object you're cleaning. 16 | * @param {Boolean} [options.filter=true] - Do filtering? 17 | * @param {Boolean} [options.autoConvert=true] - Do automatic type converting? 18 | * @param {Boolean} [options.removeEmptyStrings=true] - Remove keys in normal object or $set where the value is an empty string? 19 | * @param {Boolean} [options.removeNullsFromArrays=false] - Remove all null items from all arrays 20 | * @param {Boolean} [options.trimStrings=true] - Trim string values? 21 | * @param {Boolean} [options.getAutoValues=true] - Inject automatic and default values? 22 | * @param {Boolean} [options.isModifier=false] - Is doc a modifier object? 23 | * @param {Boolean} [options.isUpsert=false] - Will the modifier object be used to do an upsert? This is used 24 | * to determine whether $setOnInsert should be added to it for defaultValues. 25 | * @param {Boolean} [options.mongoObject] - If you already have the mongoObject instance, pass it to improve performance 26 | * @param {Object} [options.extendAutoValueContext] - This object will be added to the `this` context of autoValue functions. 27 | * @returns {Object} The modified doc. 28 | * 29 | * Cleans a document or modifier object. By default, will filter, automatically 30 | * type convert where possible, and inject automatic/default values. Use the options 31 | * to skip one or more of these. 32 | */ 33 | function clean(ss, doc, options = {}) { 34 | // By default, doc will be filtered and autoconverted 35 | options = { 36 | isModifier: looksLikeModifier(doc), 37 | isUpsert: false, 38 | ...ss._cleanOptions, 39 | ...options, 40 | }; 41 | 42 | // Clone so we do not mutate 43 | const cleanDoc = options.mutate ? doc : clone(doc); 44 | 45 | const mongoObject = options.mongoObject || new MongoObject(cleanDoc, ss.blackboxKeys()); 46 | 47 | // Clean loop 48 | if ( 49 | options.filter 50 | || options.autoConvert 51 | || options.removeEmptyStrings 52 | || options.trimStrings 53 | ) { 54 | const removedPositions = []; // For removing now-empty objects after 55 | 56 | mongoObject.forEachNode( 57 | function eachNode() { 58 | // The value of a $unset is irrelevant, so no point in cleaning it. 59 | // Also we do not care if fields not in the schema are unset. 60 | // Other operators also have values that we wouldn't want to clean. 61 | if (operatorsToIgnoreValue.includes(this.operator)) return; 62 | 63 | const gKey = this.genericKey; 64 | if (!gKey) return; 65 | 66 | let val = this.value; 67 | if (val === undefined) return; 68 | 69 | let p; 70 | 71 | // Filter out props if necessary 72 | if ( 73 | (options.filter && !ss.allowsKey(gKey)) 74 | || (options.removeNullsFromArrays && this.isArrayItem && val === null) 75 | ) { 76 | // XXX Special handling for $each; maybe this could be made nicer 77 | if (this.position.slice(-7) === '[$each]') { 78 | mongoObject.removeValueForPosition(this.position.slice(0, -7)); 79 | removedPositions.push(this.position.slice(0, -7)); 80 | } else { 81 | this.remove(); 82 | removedPositions.push(this.position); 83 | } 84 | if (SimpleSchema.debug) { 85 | console.info( 86 | `SimpleSchema.clean: filtered out value that would have affected key "${gKey}", which is not allowed by the schema`, 87 | ); 88 | } 89 | return; // no reason to do more 90 | } 91 | 92 | const outerDef = ss.schema(gKey); 93 | const defs = outerDef && outerDef.type.definitions; 94 | const def = defs && defs[0]; 95 | 96 | // Autoconvert values if requested and if possible 97 | if (options.autoConvert && def) { 98 | const isValidType = defs.some((definition) => { 99 | const errors = typeValidator.call({ 100 | valueShouldBeChecked: true, 101 | definition, 102 | value: val, 103 | }); 104 | return errors === undefined; 105 | }); 106 | 107 | if (!isValidType) { 108 | const newVal = convertToProperType(val, def.type); 109 | if (newVal !== undefined && newVal !== val) { 110 | SimpleSchema.debug 111 | && console.info( 112 | `SimpleSchema.clean: autoconverted value ${val} from ${typeof val} to ${typeof newVal} for ${gKey}`, 113 | ); 114 | val = newVal; 115 | this.updateValue(newVal); 116 | } 117 | } 118 | } 119 | 120 | // Trim strings if 121 | // 1. The trimStrings option is `true` AND 122 | // 2. The field is not in the schema OR is in the schema with `trim` !== `false` AND 123 | // 3. The value is a string. 124 | if ( 125 | options.trimStrings 126 | && (!def || def.trim !== false) 127 | && typeof val === 'string' 128 | ) { 129 | val = val.trim(); 130 | this.updateValue(val); 131 | } 132 | 133 | // Remove empty strings if 134 | // 1. The removeEmptyStrings option is `true` AND 135 | // 2. The value is in a normal object or in the $set part of a modifier 136 | // 3. The value is an empty string. 137 | if ( 138 | options.removeEmptyStrings 139 | && (!this.operator || this.operator === '$set') 140 | && typeof val === 'string' 141 | && !val.length 142 | ) { 143 | // For a document, we remove any fields that are being set to an empty string 144 | this.remove(); 145 | // For a modifier, we $unset any fields that are being set to an empty string. 146 | // But only if we're not already within an entire object that is being set. 147 | if ( 148 | this.operator === '$set' 149 | && this.position.match(/\[/g).length < 2 150 | ) { 151 | p = this.position.replace('$set', '$unset'); 152 | mongoObject.setValueForPosition(p, ''); 153 | } 154 | } 155 | }, 156 | { endPointsOnly: false }, 157 | ); 158 | 159 | // Remove any objects that are now empty after filtering 160 | removedPositions.forEach((removedPosition) => { 161 | const lastBrace = removedPosition.lastIndexOf('['); 162 | if (lastBrace !== -1) { 163 | const removedPositionParent = removedPosition.slice(0, lastBrace); 164 | const value = mongoObject.getValueForPosition(removedPositionParent); 165 | if (isEmptyObject(value)) mongoObject.removeValueForPosition(removedPositionParent); 166 | } 167 | }); 168 | 169 | mongoObject.removeArrayItems(); 170 | } 171 | 172 | // Set automatic values 173 | options.getAutoValues 174 | && setAutoValues( 175 | ss.autoValueFunctions(), 176 | mongoObject, 177 | options.isModifier, 178 | options.isUpsert, 179 | options.extendAutoValueContext, 180 | ); 181 | 182 | // Ensure we don't have any operators set to an empty object 183 | // since MongoDB 2.6+ will throw errors. 184 | if (options.isModifier) { 185 | Object.keys(cleanDoc || {}).forEach((op) => { 186 | const operatorValue = cleanDoc[op]; 187 | if ( 188 | typeof operatorValue === 'object' 189 | && operatorValue !== null 190 | && isEmptyObject(operatorValue) 191 | ) { 192 | delete cleanDoc[op]; 193 | } 194 | }); 195 | } 196 | 197 | return cleanDoc; 198 | } 199 | 200 | export default clean; 201 | -------------------------------------------------------------------------------- /lib/SimpleSchema_extend.tests.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { SimpleSchema, schemaDefinitionOptions } from './SimpleSchema'; 3 | import expectErrorOfTypeLength from './testHelpers/expectErrorOfTypeLength'; 4 | 5 | describe('SimpleSchema - Extend Schema', function () { 6 | it('supports extending with no type', function () { 7 | const schema = new SimpleSchema({ 8 | name: { 9 | type: String, 10 | min: 5, 11 | }, 12 | }); 13 | schema.extend({ 14 | name: { 15 | max: 15, 16 | }, 17 | }); 18 | expect(schema.get('name', 'max')).to.equal(15); 19 | }); 20 | 21 | it('updates blackbox keys after extending', function () { 22 | const schema = new SimpleSchema({ 23 | apple: { 24 | type: Object, 25 | blackbox: true, 26 | }, 27 | pear: new SimpleSchema({ 28 | info: { 29 | type: Object, 30 | blackbox: true, 31 | }, 32 | }), 33 | }); 34 | 35 | schema.extend({ 36 | pear: String, 37 | }); 38 | 39 | expect(schema.blackboxKeys()).to.deep.equal(['apple']); 40 | }); 41 | 42 | it('works for plain objects', function () { 43 | const schema = new SimpleSchema({ 44 | firstName: { 45 | type: String, 46 | label: 'First name', 47 | optional: false, 48 | }, 49 | lastName: { 50 | type: String, 51 | label: 'Last name', 52 | optional: false, 53 | }, 54 | }); 55 | 56 | schema.extend({ 57 | firstName: { 58 | optional: true, 59 | }, 60 | }); 61 | 62 | expect(schema.schema()).to.deep.equal({ 63 | firstName: { 64 | type: SimpleSchema.oneOf(String), 65 | label: 'First name', 66 | optional: true, 67 | }, 68 | lastName: { 69 | type: SimpleSchema.oneOf(String), 70 | label: 'Last name', 71 | optional: false, 72 | }, 73 | }); 74 | }); 75 | 76 | it('works for another SimpleSchema instance and copies validators', function () { 77 | const schema1 = new SimpleSchema({ 78 | firstName: { 79 | type: String, 80 | label: 'First name', 81 | optional: false, 82 | }, 83 | lastName: { 84 | type: String, 85 | label: 'Last name', 86 | optional: false, 87 | }, 88 | }); 89 | 90 | const schema2 = new SimpleSchema({ 91 | age: { 92 | type: Number, 93 | label: 'Age', 94 | }, 95 | }); 96 | schema2.addValidator(() => {}); 97 | schema2.addDocValidator(() => {}); 98 | 99 | expect(schema1.schema()).to.deep.equal({ 100 | firstName: { 101 | type: SimpleSchema.oneOf(String), 102 | label: 'First name', 103 | optional: false, 104 | }, 105 | lastName: { 106 | type: SimpleSchema.oneOf(String), 107 | label: 'Last name', 108 | optional: false, 109 | }, 110 | }); 111 | expect(schema1._validators.length).to.equal(0); 112 | expect(schema1._docValidators.length).to.equal(0); 113 | 114 | schema1.extend(schema2); 115 | 116 | expect(schema1.schema()).to.deep.equal({ 117 | firstName: { 118 | type: SimpleSchema.oneOf(String), 119 | label: 'First name', 120 | optional: false, 121 | }, 122 | lastName: { 123 | type: SimpleSchema.oneOf(String), 124 | label: 'Last name', 125 | optional: false, 126 | }, 127 | age: { 128 | type: SimpleSchema.oneOf(Number), 129 | label: 'Age', 130 | optional: false, 131 | }, 132 | }); 133 | expect(schema1._validators.length).to.equal(1); 134 | expect(schema1._docValidators.length).to.equal(1); 135 | }); 136 | 137 | it('keeps both min and max', function () { 138 | const schema = new SimpleSchema({ 139 | name: { 140 | type: String, 141 | min: 5, 142 | }, 143 | }); 144 | schema.extend({ 145 | name: { 146 | type: String, 147 | max: 15, 148 | }, 149 | }); 150 | 151 | expect(schema._schema.name.type.definitions[0].min).to.equal(5); 152 | expect(schema._schema.name.type.definitions[0].max).to.equal(15); 153 | }); 154 | 155 | it('does not mutate a schema that is passed to extend', function () { 156 | const itemSchema = new SimpleSchema({ 157 | _id: String, 158 | }); 159 | const mainSchema = new SimpleSchema({ 160 | items: Array, 161 | 'items.$': itemSchema, 162 | }); 163 | 164 | const item2Schema = new SimpleSchema({ 165 | blah: String, 166 | }); 167 | const main2Schema = new SimpleSchema({ 168 | items: Array, 169 | 'items.$': item2Schema, 170 | }); 171 | 172 | new SimpleSchema({}).extend(mainSchema).extend(main2Schema); 173 | 174 | expect(mainSchema._schema['items.$'].type.definitions[0].type._schemaKeys).to.deep.equal(['_id']); 175 | }); 176 | 177 | it('can extend array definition only, without array item definition', function () { 178 | const schema = new SimpleSchema({ 179 | myArray: { 180 | type: Array, 181 | }, 182 | 'myArray.$': { 183 | type: String, 184 | allowedValues: ['foo', 'bar'], 185 | }, 186 | }); 187 | 188 | expect(schema._schema.myArray.type.definitions[0].minCount).to.equal(undefined); 189 | 190 | schema.extend({ 191 | myArray: { 192 | minCount: 1, 193 | }, 194 | }); 195 | 196 | expect(schema._schema.myArray.type.definitions[0].minCount).to.equal(1); 197 | }); 198 | 199 | it('tests requiredness on fields added through extension', function () { 200 | const subitemSchema = new SimpleSchema({ 201 | name: String, 202 | }); 203 | 204 | const itemSchema = new SimpleSchema({ 205 | name: String, 206 | subitems: { 207 | type: Array, 208 | optional: true, 209 | }, 210 | 'subitems.$': { 211 | type: subitemSchema, 212 | }, 213 | }); 214 | 215 | const schema = new SimpleSchema({ 216 | name: String, 217 | items: { 218 | type: Array, 219 | optional: true, 220 | }, 221 | 'items.$': { 222 | type: itemSchema, 223 | }, 224 | }); 225 | 226 | subitemSchema.extend({ 227 | other: String, 228 | }); 229 | 230 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.REQUIRED, schema, { 231 | name: 'foo', 232 | items: [{ 233 | name: 'foo', 234 | subitems: [{ 235 | name: 'foo', 236 | }], 237 | }], 238 | }).to.equal(1); 239 | }); 240 | 241 | it('gets correct objectKeys from extended subschemas', function () { 242 | const itemSchema = new SimpleSchema({ 243 | name: String, 244 | }); 245 | 246 | const schema = new SimpleSchema({ 247 | name: String, 248 | item: itemSchema, 249 | }); 250 | 251 | itemSchema.extend({ 252 | other: String, 253 | }); 254 | 255 | expect(schema.objectKeys()).to.deep.equal(['name', 'item']); 256 | expect(schema.objectKeys('item')).to.deep.equal(['name', 'other']); 257 | }); 258 | 259 | it('supports extending allowedValues', function () { 260 | const ObjSchema = new SimpleSchema({ 261 | loveType: { 262 | type: String, 263 | allowedValues: ['platonic'] 264 | } 265 | }); 266 | 267 | const ListItemSchema = new SimpleSchema({ 268 | name: { 269 | type: String, 270 | allowedValues: ['a'] 271 | }, 272 | params: { 273 | type: Object, 274 | blackbox: true 275 | } 276 | }); 277 | 278 | const schema = new SimpleSchema({ 279 | 'list': { 280 | type: Array 281 | }, 282 | 'list.$': { 283 | type: ListItemSchema 284 | }, 285 | 'primary': ObjSchema, 286 | 'method': { 287 | type: String, 288 | allowedValues: ['none', 'some'] 289 | }, 290 | }); 291 | 292 | // Top-level field extension 293 | schema.extend({ 294 | method: { 295 | allowedValues: [...schema.getAllowedValuesForKey('method'), 'all'] 296 | }, 297 | }); 298 | 299 | expect(schema.getAllowedValuesForKey('method')).to.deep.equal(['none', 'some', 'all']); 300 | 301 | // Subschema field extension 302 | ObjSchema.extend({ 303 | loveType: { 304 | allowedValues: [...ObjSchema.getAllowedValuesForKey('loveType'), 'romantic'] 305 | }, 306 | }); 307 | 308 | expect(schema.getAllowedValuesForKey('primary.loveType')).to.deep.equal(['platonic', 'romantic']); 309 | 310 | // Subschema field in array field extension 311 | ListItemSchema.extend({ 312 | name: { 313 | allowedValues: [...ListItemSchema.getAllowedValuesForKey('name'), 'b', 'c'] 314 | }, 315 | }); 316 | 317 | expect(schema.getAllowedValuesForKey('list.$.name')).to.deep.equal(['a', 'b', 'c']); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /lib/SimpleSchema_min.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import testSchema from './testHelpers/testSchema'; 4 | import expectErrorLength from './testHelpers/expectErrorLength'; 5 | 6 | describe('SimpleSchema - min', function () { 7 | describe('normal', function () { 8 | it('string', function () { 9 | expectErrorLength(testSchema, { 10 | minMaxString: 'longenough', 11 | }).to.deep.equal(0); 12 | 13 | expectErrorLength(testSchema, { 14 | minMaxString: 'short', 15 | }).to.deep.equal(1); 16 | 17 | expectErrorLength(testSchema, { 18 | minMaxString: '', 19 | }).to.deep.equal(1); 20 | 21 | expectErrorLength(testSchema, { 22 | minMaxStringArray: ['longenough', 'longenough'], 23 | }).to.deep.equal(0); 24 | 25 | expectErrorLength(testSchema, { 26 | minMaxStringArray: ['longenough', 'short'], 27 | }).to.deep.equal(1); 28 | 29 | expectErrorLength(testSchema, { 30 | minMaxStringArray: ['short', 'short'], 31 | }).to.deep.equal(2); 32 | }); 33 | 34 | it('number', function () { 35 | expectErrorLength(testSchema, { 36 | minMaxNumberExclusive: 20, 37 | }).to.deep.equal(1); 38 | 39 | expectErrorLength(testSchema, { 40 | minMaxNumberExclusive: 10, 41 | }).to.deep.equal(1); 42 | 43 | expectErrorLength(testSchema, { 44 | minMaxNumberInclusive: 20, 45 | }).to.deep.equal(0); 46 | 47 | expectErrorLength(testSchema, { 48 | minMaxNumberInclusive: 10, 49 | }).to.deep.equal(0); 50 | 51 | expectErrorLength(testSchema, { 52 | minMaxNumber: 10, 53 | }).to.deep.equal(0); 54 | 55 | expectErrorLength(testSchema, { 56 | minMaxNumber: 9, 57 | }).to.deep.equal(1); 58 | 59 | expectErrorLength(testSchema, { 60 | minMaxNumberCalculated: 10, 61 | }).to.deep.equal(0); 62 | 63 | expectErrorLength(testSchema, { 64 | minMaxNumberCalculated: 9, 65 | }).to.deep.equal(1); 66 | 67 | expectErrorLength(testSchema, { 68 | minZero: -1, 69 | }).to.deep.equal(1); 70 | }); 71 | 72 | it('date', function () { 73 | expectErrorLength(testSchema, { 74 | minMaxDate: (new Date(Date.UTC(2013, 0, 1))), 75 | }).to.deep.equal(0); 76 | 77 | expectErrorLength(testSchema, { 78 | minMaxDate: (new Date(Date.UTC(2012, 11, 31))), 79 | }).to.deep.equal(1); 80 | 81 | expectErrorLength(testSchema, { 82 | minMaxDateCalculated: (new Date(Date.UTC(2013, 0, 1))), 83 | }).to.deep.equal(0); 84 | 85 | expectErrorLength(testSchema, { 86 | minMaxDateCalculated: (new Date(Date.UTC(2012, 11, 31))), 87 | }).to.deep.equal(1); 88 | }); 89 | }); 90 | 91 | describe('modifier with $setOnInsert', function () { 92 | it('string', function () { 93 | expectErrorLength(testSchema, { 94 | $setOnInsert: { 95 | minMaxString: 'longenough', 96 | }, 97 | }, { modifier: true, upsert: true }).to.deep.equal(0); 98 | 99 | expectErrorLength(testSchema, { 100 | $setOnInsert: { 101 | minMaxString: 'short', 102 | }, 103 | }, { modifier: true, upsert: true }).to.deep.equal(1); 104 | 105 | expectErrorLength(testSchema, { 106 | $setOnInsert: { 107 | minMaxStringArray: ['longenough', 'longenough'], 108 | }, 109 | }, { modifier: true, upsert: true }).to.deep.equal(0); 110 | 111 | expectErrorLength(testSchema, { 112 | $setOnInsert: { 113 | minMaxStringArray: ['longenough', 'short'], 114 | }, 115 | }, { modifier: true, upsert: true }).to.deep.equal(1); 116 | 117 | expectErrorLength(testSchema, { 118 | $setOnInsert: { 119 | minMaxStringArray: ['short', 'short'], 120 | }, 121 | }, { modifier: true, upsert: true }).to.deep.equal(2); 122 | }); 123 | 124 | it('number', function () { 125 | expectErrorLength(testSchema, { 126 | $setOnInsert: { 127 | minMaxNumber: 10, 128 | }, 129 | }, { modifier: true, upsert: true }).to.deep.equal(0); 130 | 131 | expectErrorLength(testSchema, { 132 | $setOnInsert: { 133 | minMaxNumber: 9, 134 | }, 135 | }, { modifier: true, upsert: true }).to.deep.equal(1); 136 | 137 | expectErrorLength(testSchema, { 138 | $setOnInsert: { 139 | minMaxNumberCalculated: 10, 140 | }, 141 | }, { modifier: true, upsert: true }).to.deep.equal(0); 142 | 143 | expectErrorLength(testSchema, { 144 | $setOnInsert: { 145 | minMaxNumberCalculated: 9, 146 | }, 147 | }, { modifier: true, upsert: true }).to.deep.equal(1); 148 | 149 | expectErrorLength(testSchema, { 150 | $setOnInsert: { 151 | minZero: -1, 152 | }, 153 | }, { modifier: true, upsert: true }).to.deep.equal(1); 154 | }); 155 | 156 | it('date', function () { 157 | expectErrorLength(testSchema, { 158 | $setOnInsert: { 159 | minMaxDate: (new Date(Date.UTC(2013, 0, 1))), 160 | }, 161 | }, { modifier: true, upsert: true }).to.deep.equal(0); 162 | 163 | expectErrorLength(testSchema, { 164 | $setOnInsert: { 165 | minMaxDate: (new Date(Date.UTC(2012, 11, 31))), 166 | }, 167 | }, { modifier: true, upsert: true }).to.deep.equal(1); 168 | 169 | expectErrorLength(testSchema, { 170 | $setOnInsert: { 171 | minMaxDateCalculated: (new Date(Date.UTC(2013, 0, 1))), 172 | }, 173 | }, { modifier: true, upsert: true }).to.deep.equal(0); 174 | 175 | expectErrorLength(testSchema, { 176 | $setOnInsert: { 177 | minMaxDateCalculated: (new Date(Date.UTC(2012, 11, 31))), 178 | }, 179 | }, { modifier: true, upsert: true }).to.deep.equal(1); 180 | }); 181 | }); 182 | 183 | describe('modifier with $set or $inc', function () { 184 | it('string', function () { 185 | expectErrorLength(testSchema, { 186 | $set: { 187 | minMaxString: 'longenough', 188 | }, 189 | }, { modifier: true }).to.deep.equal(0); 190 | 191 | expectErrorLength(testSchema, { 192 | $set: { 193 | minMaxString: 'short', 194 | }, 195 | }, { modifier: true }).to.deep.equal(1); 196 | 197 | expectErrorLength(testSchema, { 198 | $set: { 199 | minMaxStringArray: ['longenough', 'longenough'], 200 | }, 201 | }, { modifier: true }).to.deep.equal(0); 202 | 203 | expectErrorLength(testSchema, { 204 | $set: { 205 | minMaxStringArray: ['longenough', 'short'], 206 | }, 207 | }, { modifier: true }).to.deep.equal(1); 208 | 209 | expectErrorLength(testSchema, { 210 | $set: { 211 | minMaxStringArray: ['short', 'short'], 212 | }, 213 | }, { modifier: true }).to.deep.equal(2); 214 | }); 215 | 216 | it('number', function () { 217 | expectErrorLength(testSchema, { 218 | $set: { 219 | minMaxNumber: 10, 220 | }, 221 | }, { modifier: true }).to.deep.equal(0); 222 | 223 | expectErrorLength(testSchema, { 224 | $set: { 225 | minMaxNumber: 9, 226 | }, 227 | }, { modifier: true }).to.deep.equal(1); 228 | 229 | expectErrorLength(testSchema, { 230 | $set: { 231 | minMaxNumberCalculated: 10, 232 | }, 233 | }, { modifier: true }).to.deep.equal(0); 234 | 235 | expectErrorLength(testSchema, { 236 | $set: { 237 | minMaxNumberCalculated: 9, 238 | }, 239 | }, { modifier: true }).to.deep.equal(1); 240 | 241 | expectErrorLength(testSchema, { 242 | $set: { 243 | minZero: -1, 244 | }, 245 | }, { modifier: true }).to.deep.equal(1); 246 | 247 | // Should not be invalid because we don't know what we're starting from 248 | expectErrorLength(testSchema, { 249 | $inc: { 250 | minZero: -5, 251 | }, 252 | }, { modifier: true }).to.deep.equal(0); 253 | }); 254 | 255 | it('date', function () { 256 | expectErrorLength(testSchema, { 257 | $set: { 258 | minMaxDate: (new Date(Date.UTC(2013, 0, 1))), 259 | }, 260 | }, { modifier: true }).to.deep.equal(0); 261 | 262 | expectErrorLength(testSchema, { 263 | $set: { 264 | minMaxDate: (new Date(Date.UTC(2012, 11, 31))), 265 | }, 266 | }, { modifier: true }).to.deep.equal(1); 267 | 268 | expectErrorLength(testSchema, { 269 | $set: { 270 | minMaxDateCalculated: (new Date(Date.UTC(2013, 0, 1))), 271 | }, 272 | }, { modifier: true }).to.deep.equal(0); 273 | 274 | expectErrorLength(testSchema, { 275 | $set: { 276 | minMaxDateCalculated: (new Date(Date.UTC(2012, 11, 31))), 277 | }, 278 | }, { modifier: true }).to.deep.equal(1); 279 | }); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /lib/SimpleSchema_messages.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | describe('messages', function () { 8 | it('required', function () { 9 | const schema = new SimpleSchema({ 10 | foo: String, 11 | }); 12 | 13 | const context = schema.newContext(); 14 | context.validate({}); 15 | expect(context.keyErrorMessage('foo')).to.equal('Foo is required'); 16 | }); 17 | 18 | it('minString', function () { 19 | const schema = new SimpleSchema({ 20 | foo: { 21 | type: String, 22 | min: 2, 23 | }, 24 | }); 25 | 26 | const context = schema.newContext(); 27 | context.validate({ foo: 'a' }); 28 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be at least 2 characters'); 29 | }); 30 | 31 | it('maxString', function () { 32 | const schema = new SimpleSchema({ 33 | foo: { 34 | type: String, 35 | max: 2, 36 | }, 37 | }); 38 | 39 | const context = schema.newContext(); 40 | context.validate({ foo: 'abc' }); 41 | expect(context.keyErrorMessage('foo')).to.equal('Foo cannot exceed 2 characters'); 42 | }); 43 | 44 | it('minNumber', function () { 45 | const schema = new SimpleSchema({ 46 | foo: { 47 | type: Number, 48 | min: 2, 49 | }, 50 | }); 51 | 52 | const context = schema.newContext(); 53 | context.validate({ foo: 1.5 }); 54 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be at least 2'); 55 | }); 56 | 57 | it('maxNumber', function () { 58 | const schema = new SimpleSchema({ 59 | foo: { 60 | type: Number, 61 | max: 2, 62 | }, 63 | }); 64 | 65 | const context = schema.newContext(); 66 | context.validate({ foo: 2.5 }); 67 | expect(context.keyErrorMessage('foo')).to.equal('Foo cannot exceed 2'); 68 | }); 69 | 70 | it('minNumberExclusive', function () { 71 | const schema = new SimpleSchema({ 72 | foo: { 73 | type: Number, 74 | min: 2, 75 | exclusiveMin: true, 76 | }, 77 | }); 78 | 79 | const context = schema.newContext(); 80 | context.validate({ foo: 2 }); 81 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be greater than 2'); 82 | }); 83 | 84 | it('maxNumberExclusive', function () { 85 | const schema = new SimpleSchema({ 86 | foo: { 87 | type: Number, 88 | max: 2, 89 | exclusiveMax: true, 90 | }, 91 | }); 92 | 93 | const context = schema.newContext(); 94 | context.validate({ foo: 2 }); 95 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be less than 2'); 96 | }); 97 | 98 | it('minDate', function () { 99 | const schema = new SimpleSchema({ 100 | foo: { 101 | type: Date, 102 | min: new Date(Date.UTC(2015, 11, 15, 0, 0, 0, 0)), 103 | }, 104 | }); 105 | 106 | const context = schema.newContext(); 107 | context.validate({ foo: new Date(Date.UTC(2015, 10, 15, 0, 0, 0, 0)) }); 108 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be on or after 2015-12-15'); 109 | }); 110 | 111 | it('maxDate', function () { 112 | const schema = new SimpleSchema({ 113 | foo: { 114 | type: Date, 115 | max: new Date(Date.UTC(2015, 11, 15, 0, 0, 0, 0)), 116 | }, 117 | }); 118 | 119 | const context = schema.newContext(); 120 | context.validate({ foo: new Date(Date.UTC(2016, 1, 15, 0, 0, 0, 0)) }); 121 | expect(context.keyErrorMessage('foo')).to.equal('Foo cannot be after 2015-12-15'); 122 | }); 123 | 124 | it('badDate', function () { 125 | const schema = new SimpleSchema({ 126 | foo: { 127 | type: Date, 128 | }, 129 | }); 130 | 131 | const context = schema.newContext(); 132 | context.validate({ foo: new Date('invalid') }); 133 | expect(context.keyErrorMessage('foo')).to.equal('Foo is not a valid date'); 134 | }); 135 | 136 | it('minCount', function () { 137 | const schema = new SimpleSchema({ 138 | foo: { 139 | type: Array, 140 | minCount: 2, 141 | }, 142 | 'foo.$': Number, 143 | }); 144 | 145 | const context = schema.newContext(); 146 | context.validate({ foo: [1] }); 147 | expect(context.keyErrorMessage('foo')).to.equal('You must specify at least 2 values'); 148 | }); 149 | 150 | it('maxCount', function () { 151 | const schema = new SimpleSchema({ 152 | foo: { 153 | type: Array, 154 | maxCount: 2, 155 | }, 156 | 'foo.$': Number, 157 | }); 158 | 159 | const context = schema.newContext(); 160 | context.validate({ foo: [1, 2, 3] }); 161 | expect(context.keyErrorMessage('foo')).to.equal('You cannot specify more than 2 values'); 162 | }); 163 | 164 | it('noDecimal', function () { 165 | const schema = new SimpleSchema({ 166 | foo: SimpleSchema.Integer, 167 | }); 168 | 169 | const context = schema.newContext(); 170 | context.validate({ foo: 1.5 }); 171 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be an integer'); 172 | }); 173 | 174 | it('notAllowed', function () { 175 | const schema = new SimpleSchema({ 176 | foo: { 177 | type: String, 178 | allowedValues: ['a', 'b', 'c'], 179 | }, 180 | }); 181 | 182 | const context = schema.newContext(); 183 | context.validate({ foo: 'd' }); 184 | expect(context.keyErrorMessage('foo')).to.equal('d is not an allowed value'); 185 | }); 186 | 187 | it('expectedType', function () { 188 | const schema = new SimpleSchema({ 189 | foo: String, 190 | }); 191 | 192 | const context = schema.newContext(); 193 | context.validate({ foo: 1 }); 194 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be of type String'); 195 | }); 196 | 197 | it('regEx built in', function () { 198 | const schema = new SimpleSchema({ 199 | foo: { 200 | type: String, 201 | regEx: SimpleSchema.RegEx.Email, 202 | }, 203 | }); 204 | 205 | const context = schema.newContext(); 206 | context.validate({ foo: 'abc' }); 207 | expect(context.keyErrorMessage('foo')).to.equal('Foo must be a valid email address'); 208 | }); 209 | 210 | it('regEx other', function () { 211 | const schema = new SimpleSchema({ 212 | foo: { 213 | type: String, 214 | regEx: /def/g, 215 | }, 216 | }); 217 | 218 | const context = schema.newContext(); 219 | context.validate({ foo: 'abc' }); 220 | expect(context.keyErrorMessage('foo')).to.equal('Foo failed regular expression validation'); 221 | }); 222 | 223 | describe('keyNotInSchema', function () { 224 | const schema = new SimpleSchema({ 225 | foo: String, 226 | }); 227 | 228 | it('normal', function () { 229 | const context = schema.newContext(); 230 | context.validate({ bar: 1 }); 231 | expect(context.keyErrorMessage('bar')).to.equal('bar is not allowed by the schema'); 232 | }); 233 | 234 | it('$set', function () { 235 | const context = schema.newContext(); 236 | context.validate({ 237 | $set: { 238 | bar: 1, 239 | }, 240 | }, { modifier: true }); 241 | expect(context.keyErrorMessage('bar')).to.equal('bar is not allowed by the schema'); 242 | }); 243 | 244 | it('$unset does not complain', function () { 245 | const context = schema.newContext(); 246 | context.validate({ 247 | $unset: { 248 | bar: '', 249 | }, 250 | }, { modifier: true }); 251 | expect(context.isValid()).to.equal(true); 252 | }); 253 | }); 254 | 255 | it('should allow labels with apostrophes ("\'") in messages', function () { 256 | const schema = new SimpleSchema({ 257 | foo: { 258 | type: String, 259 | label: 'Manager/supervisor\'s name', 260 | }, 261 | }); 262 | 263 | const context = schema.newContext(); 264 | context.validate({}); 265 | expect(context.keyErrorMessage('foo')).to.equal('Manager/supervisor\'s name is required'); 266 | }); 267 | }); 268 | 269 | describe('multipleSchema', function () { 270 | const schema = new SimpleSchema({ 271 | foo: String, 272 | }); 273 | 274 | schema.messageBox.messages({ 275 | en: { 276 | required: { 277 | foo: 'Your foo is required mate', 278 | }, 279 | }, 280 | }); 281 | 282 | const schema2 = new SimpleSchema({ 283 | foo: String, 284 | bar: Number, 285 | }); 286 | 287 | schema2.messageBox.messages({ 288 | en: { 289 | required: { 290 | foo: 'Your bar is required for sure', 291 | }, 292 | }, 293 | }); 294 | 295 | it('should keep message boxes separate between objects', function () { 296 | const context = schema.newContext(); 297 | context.validate({}); 298 | expect(context.keyErrorMessage('foo')).to.equal('Your foo is required mate'); 299 | }); 300 | }); 301 | }); 302 | -------------------------------------------------------------------------------- /lib/SimpleSchema_max.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import testSchema from './testHelpers/testSchema'; 4 | import friendsSchema from './testHelpers/friendsSchema'; 5 | import expectErrorLength from './testHelpers/expectErrorLength'; 6 | import expectErrorOfTypeLength from './testHelpers/expectErrorOfTypeLength'; 7 | import { SimpleSchema } from './SimpleSchema'; 8 | 9 | describe('SimpleSchema - max', function () { 10 | describe('normal', function () { 11 | it('string', function () { 12 | expectErrorLength(testSchema, { 13 | minMaxString: 'nottoolongnottoolong', 14 | }).to.deep.equal(0); 15 | 16 | expectErrorLength(testSchema, { 17 | minMaxString: 'toolongtoolongtoolong', 18 | }).to.deep.equal(1); 19 | 20 | expectErrorLength(testSchema, { 21 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong'], 22 | }).to.deep.equal(0); 23 | 24 | expectErrorLength(testSchema, { 25 | minMaxStringArray: ['toolongtoolongtoolong', 'toolongtoolongtoolong'], 26 | }).to.deep.equal(2); 27 | 28 | expectErrorLength(testSchema, { 29 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong', 'nottoolongnottoolong'], 30 | }).to.deep.equal(1); 31 | }); 32 | 33 | it('number', function () { 34 | expectErrorLength(testSchema, { 35 | minMaxNumber: 20, 36 | }).to.deep.equal(0); 37 | 38 | expectErrorLength(testSchema, { 39 | minMaxNumber: 21, 40 | }).to.deep.equal(1); 41 | 42 | expectErrorLength(testSchema, { 43 | minMaxNumberCalculated: 20, 44 | }).to.deep.equal(0); 45 | 46 | expectErrorLength(testSchema, { 47 | minMaxNumberCalculated: 21, 48 | }).to.deep.equal(1); 49 | }); 50 | 51 | it('date', function () { 52 | expectErrorLength(testSchema, { 53 | minMaxDate: (new Date(Date.UTC(2013, 11, 31))), 54 | }).to.deep.equal(0); 55 | 56 | expectErrorLength(testSchema, { 57 | minMaxDate: (new Date(Date.UTC(2014, 0, 1))), 58 | }).to.deep.equal(1); 59 | 60 | expectErrorLength(testSchema, { 61 | minMaxDateCalculated: (new Date(Date.UTC(2013, 11, 31))), 62 | }).to.deep.equal(0); 63 | 64 | expectErrorLength(testSchema, { 65 | minMaxDateCalculated: (new Date(Date.UTC(2014, 0, 1))), 66 | }).to.deep.equal(1); 67 | }); 68 | }); 69 | 70 | describe('modifier with $setOnInsert', function () { 71 | it('string', function () { 72 | expectErrorLength(testSchema, { 73 | $setOnInsert: { 74 | minMaxString: 'nottoolongnottoolong', 75 | }, 76 | }, { modifier: true, upsert: true }).to.deep.equal(0); 77 | 78 | expectErrorLength(testSchema, { 79 | $setOnInsert: { 80 | minMaxString: 'toolongtoolongtoolong', 81 | }, 82 | }, { modifier: true, upsert: true }).to.deep.equal(1); 83 | 84 | expectErrorLength(testSchema, { 85 | $setOnInsert: { 86 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong'], 87 | }, 88 | }, { modifier: true, upsert: true }).to.deep.equal(0); 89 | 90 | expectErrorLength(testSchema, { 91 | $setOnInsert: { 92 | minMaxStringArray: ['toolongtoolongtoolong', 'toolongtoolongtoolong'], 93 | }, 94 | }, { modifier: true, upsert: true }).to.deep.equal(2); 95 | 96 | expectErrorLength(testSchema, { 97 | $setOnInsert: { 98 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong', 'nottoolongnottoolong'], 99 | }, 100 | }, { modifier: true, upsert: true }).to.deep.equal(1); 101 | }); 102 | 103 | it('number', function () { 104 | expectErrorLength(testSchema, { 105 | $setOnInsert: { 106 | minMaxNumber: 20, 107 | }, 108 | }, { modifier: true, upsert: true }).to.deep.equal(0); 109 | 110 | expectErrorLength(testSchema, { 111 | $setOnInsert: { 112 | minMaxNumber: 21, 113 | }, 114 | }, { modifier: true, upsert: true }).to.deep.equal(1); 115 | 116 | expectErrorLength(testSchema, { 117 | $setOnInsert: { 118 | minMaxNumberCalculated: 20, 119 | }, 120 | }, { modifier: true, upsert: true }).to.deep.equal(0); 121 | 122 | expectErrorLength(testSchema, { 123 | $setOnInsert: { 124 | minMaxNumberCalculated: 21, 125 | }, 126 | }, { modifier: true, upsert: true }).to.deep.equal(1); 127 | }); 128 | 129 | it('date', function () { 130 | expectErrorLength(testSchema, { 131 | $setOnInsert: { 132 | minMaxDate: (new Date(Date.UTC(2013, 11, 31))), 133 | }, 134 | }, { modifier: true, upsert: true }).to.deep.equal(0); 135 | 136 | expectErrorLength(testSchema, { 137 | $setOnInsert: { 138 | minMaxDate: (new Date(Date.UTC(2014, 0, 1))), 139 | }, 140 | }, { modifier: true, upsert: true }).to.deep.equal(1); 141 | 142 | expectErrorLength(testSchema, { 143 | $setOnInsert: { 144 | minMaxDateCalculated: (new Date(Date.UTC(2013, 11, 31))), 145 | }, 146 | }, { modifier: true, upsert: true }).to.deep.equal(0); 147 | 148 | expectErrorLength(testSchema, { 149 | $setOnInsert: { 150 | minMaxDateCalculated: (new Date(Date.UTC(2014, 0, 1))), 151 | }, 152 | }, { modifier: true, upsert: true }).to.deep.equal(1); 153 | }); 154 | }); 155 | 156 | describe('modifier with $set or $inc', function () { 157 | it('string', function () { 158 | expectErrorLength(testSchema, { 159 | $set: { 160 | minMaxString: 'nottoolongnottoolong', 161 | }, 162 | }, { modifier: true }).to.deep.equal(0); 163 | 164 | expectErrorLength(testSchema, { 165 | $set: { 166 | minMaxString: 'toolongtoolongtoolong', 167 | }, 168 | }, { modifier: true }).to.deep.equal(1); 169 | 170 | expectErrorLength(testSchema, { 171 | $set: { 172 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong'], 173 | }, 174 | }, { modifier: true }).to.deep.equal(0); 175 | 176 | expectErrorLength(testSchema, { 177 | $set: { 178 | minMaxStringArray: ['toolongtoolongtoolong', 'toolongtoolongtoolong'], 179 | }, 180 | }, { modifier: true }).to.deep.equal(2); 181 | 182 | expectErrorLength(testSchema, { 183 | $set: { 184 | minMaxStringArray: ['nottoolongnottoolong', 'nottoolongnottoolong', 'nottoolongnottoolong'], 185 | }, 186 | }, { modifier: true }).to.deep.equal(1); 187 | }); 188 | 189 | it('number', function () { 190 | expectErrorLength(testSchema, { 191 | $set: { 192 | minMaxNumber: 20, 193 | }, 194 | }, { modifier: true }).to.deep.equal(0); 195 | 196 | expectErrorLength(testSchema, { 197 | $set: { 198 | minMaxNumber: 21, 199 | }, 200 | }, { modifier: true }).to.deep.equal(1); 201 | 202 | expectErrorLength(testSchema, { 203 | $set: { 204 | minMaxNumberCalculated: 20, 205 | }, 206 | }, { modifier: true }).to.deep.equal(0); 207 | 208 | expectErrorLength(testSchema, { 209 | $set: { 210 | minMaxNumberCalculated: 21, 211 | }, 212 | }, { modifier: true }).to.deep.equal(1); 213 | 214 | expectErrorLength(testSchema, { 215 | $set: { 216 | maxZero: 1, 217 | }, 218 | }, { modifier: true }).to.deep.equal(1); 219 | 220 | // Should not be invalid because we don't know what we're starting from 221 | expectErrorLength(testSchema, { 222 | $inc: { 223 | maxZero: 5, 224 | }, 225 | }, { modifier: true }).to.deep.equal(0); 226 | }); 227 | 228 | it('date', function () { 229 | expectErrorLength(testSchema, { 230 | $set: { 231 | minMaxDate: (new Date(Date.UTC(2013, 11, 31))), 232 | }, 233 | }, { modifier: true }).to.deep.equal(0); 234 | 235 | expectErrorLength(testSchema, { 236 | $set: { 237 | minMaxDate: (new Date(Date.UTC(2014, 0, 1))), 238 | }, 239 | }, { modifier: true }).to.deep.equal(1); 240 | 241 | expectErrorLength(testSchema, { 242 | $set: { 243 | minMaxDateCalculated: (new Date(Date.UTC(2013, 11, 31))), 244 | }, 245 | }, { modifier: true }).to.deep.equal(0); 246 | 247 | expectErrorLength(testSchema, { 248 | $set: { 249 | minMaxDateCalculated: (new Date(Date.UTC(2014, 0, 1))), 250 | }, 251 | }, { modifier: true }).to.deep.equal(1); 252 | }); 253 | }); 254 | 255 | describe('modifier with $push', function () { 256 | it('valid', function () { 257 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 258 | $push: { 259 | friends: { 260 | name: 'Bob', 261 | type: 'best', 262 | }, 263 | }, 264 | }, { modifier: true }).to.deep.equal(0); 265 | }); 266 | 267 | it('invalid', function () { 268 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 269 | $push: { 270 | friends: { 271 | name: 'Bobby', 272 | type: 'best', 273 | }, 274 | }, 275 | }, { modifier: true }).to.deep.equal(1); 276 | }); 277 | }); 278 | 279 | describe('modifier with $push and $each', function () { 280 | it('valid', function () { 281 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 282 | $push: { 283 | friends: { 284 | $each: [{ 285 | name: 'Bob', 286 | type: 'best', 287 | }, { 288 | name: 'Bob', 289 | type: 'best', 290 | }], 291 | }, 292 | }, 293 | }, { modifier: true }).to.deep.equal(0); 294 | }); 295 | 296 | it('invalid', function () { 297 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 298 | $push: { 299 | friends: { 300 | $each: [{ 301 | name: 'Bob', 302 | type: 'best', 303 | }, { 304 | name: 'Bobby', 305 | type: 'best', 306 | }], 307 | }, 308 | }, 309 | }, { modifier: true }).to.deep.equal(1); 310 | }); 311 | }); 312 | 313 | describe('modifier with $addToSet', function () { 314 | it('valid', function () { 315 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 316 | $addToSet: { 317 | friends: { 318 | name: 'Bob', 319 | type: 'best', 320 | }, 321 | }, 322 | }, { modifier: true }).to.deep.equal(0); 323 | }); 324 | 325 | it('invalid', function () { 326 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 327 | $addToSet: { 328 | friends: { 329 | name: 'Bobby', 330 | type: 'best', 331 | }, 332 | }, 333 | }, { modifier: true }).to.deep.equal(1); 334 | }); 335 | }); 336 | 337 | describe('modifier with $addToSet and $each', function () { 338 | it('valid', function () { 339 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 340 | $addToSet: { 341 | friends: { 342 | $each: [{ 343 | name: 'Bob', 344 | type: 'best', 345 | }, { 346 | name: 'Bob', 347 | type: 'best', 348 | }], 349 | }, 350 | }, 351 | }, { modifier: true }).to.deep.equal(0); 352 | }); 353 | 354 | it('invalid', function () { 355 | expectErrorOfTypeLength(SimpleSchema.ErrorTypes.MAX_STRING, friendsSchema, { 356 | $addToSet: { 357 | friends: { 358 | $each: [{ 359 | name: 'Bob', 360 | type: 'best', 361 | }, { 362 | name: 'Bobby', 363 | type: 'best', 364 | }], 365 | }, 366 | }, 367 | }, { modifier: true }).to.deep.equal(1); 368 | }); 369 | }); 370 | }); 371 | -------------------------------------------------------------------------------- /lib/SimpleSchema_allowedValues.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | import friendsSchema from './testHelpers/friendsSchema'; 6 | import testSchema from './testHelpers/testSchema'; 7 | import expectErrorLength from './testHelpers/expectErrorLength'; 8 | 9 | describe('SimpleSchema - allowedValues', function () { 10 | describe('normal', function () { 11 | it('valid string', function () { 12 | expectErrorLength(testSchema, { 13 | allowedStrings: 'tuna', 14 | }).to.deep.equal(0); 15 | 16 | expectErrorLength(testSchema, { 17 | allowedStringsArray: ['tuna', 'fish', 'salad'], 18 | }).to.deep.equal(0); 19 | 20 | expectErrorLength(testSchema, { 21 | allowedStringsSet: ['tuna', 'fish', 'salad'], 22 | }).to.deep.equal(0); 23 | 24 | // Array of objects 25 | expectErrorLength(friendsSchema, { 26 | friends: [{ 27 | name: 'Bob', 28 | type: 'best', 29 | }], 30 | enemies: [], 31 | }).to.deep.equal(0); 32 | }); 33 | 34 | it('invalid string', function () { 35 | expectErrorLength(testSchema, { 36 | allowedStrings: 'tunas', 37 | }).to.deep.equal(1); 38 | 39 | // Array 40 | expectErrorLength(testSchema, { 41 | allowedStringsArray: ['tuna', 'fish', 'sandwich'], 42 | }).to.deep.equal(1); 43 | 44 | // Set Or Array 45 | expectErrorLength(testSchema, { 46 | allowedStringsSet: ['tuna', 'fish', 'sandwich'], 47 | }).to.deep.equal(1); 48 | 49 | // Array of objects 50 | expectErrorLength(friendsSchema, { 51 | friends: [{ 52 | name: 'Bob', 53 | type: 'smelly', 54 | }], 55 | enemies: [], 56 | }).to.deep.equal(1); 57 | }); 58 | 59 | it('valid number', function () { 60 | expectErrorLength(testSchema, { 61 | allowedNumbers: 1, 62 | }).to.deep.equal(0); 63 | 64 | expectErrorLength(testSchema, { 65 | allowedNumbersArray: [1, 2, 3], 66 | }).to.deep.equal(0); 67 | 68 | expectErrorLength(testSchema, { 69 | allowedNumbersSet: [1, 2, 3], 70 | }).to.deep.equal(0); 71 | 72 | // Array of objects 73 | expectErrorLength(friendsSchema, { 74 | friends: [{ 75 | name: 'Bob', 76 | type: 'best', 77 | a: { 78 | b: 5000, 79 | }, 80 | }], 81 | enemies: [], 82 | }).to.deep.equal(0); 83 | }); 84 | 85 | it('invalid number', function () { 86 | expectErrorLength(testSchema, { 87 | allowedNumbers: 4, 88 | }).to.deep.equal(1); 89 | 90 | // Array 91 | expectErrorLength(testSchema, { 92 | allowedNumbersArray: [1, 2, 3, 4], 93 | }).to.deep.equal(1); 94 | 95 | // Set or Array 96 | expectErrorLength(testSchema, { 97 | allowedNumbersSet: [1, 2, 3, 4], 98 | }).to.deep.equal(1); 99 | 100 | // Array of objects 101 | expectErrorLength(friendsSchema, { 102 | friends: [{ 103 | name: 'Bob', 104 | type: 'best', 105 | a: { 106 | b: 'wrong', 107 | }, 108 | }], 109 | enemies: [], 110 | }).to.deep.equal(1); 111 | }); 112 | }); 113 | 114 | describe('modifier with $setOnInsert', function () { 115 | it('valid string', function () { 116 | expectErrorLength(testSchema, { 117 | $setOnInsert: { 118 | allowedStrings: 'tuna', 119 | }, 120 | }, { modifier: true, upsert: true }).to.deep.equal(0); 121 | 122 | // Array 123 | expectErrorLength(testSchema, { 124 | $setOnInsert: { 125 | allowedStringsArray: ['tuna', 'fish', 'salad'], 126 | }, 127 | }, { modifier: true, upsert: true }).to.deep.equal(0); 128 | 129 | // Set or Array 130 | expectErrorLength(testSchema, { 131 | $setOnInsert: { 132 | allowedStringsSet: ['tuna', 'fish', 'salad'], 133 | }, 134 | }, { modifier: true, upsert: true }).to.deep.equal(0); 135 | 136 | // Array of objects 137 | expectErrorLength(friendsSchema, { 138 | $setOnInsert: { 139 | friends: [{ 140 | name: 'Bob', 141 | type: 'best', 142 | }], 143 | enemies: [], 144 | }, 145 | }, { modifier: true, upsert: true }).to.deep.equal(0); 146 | }); 147 | 148 | it('invalid string', function () { 149 | expectErrorLength(testSchema, { 150 | $setOnInsert: { 151 | allowedStrings: 'tunas', 152 | }, 153 | }, { modifier: true, upsert: true }).to.deep.equal(1); 154 | 155 | // Array 156 | expectErrorLength(testSchema, { 157 | $setOnInsert: { 158 | allowedStringsArray: ['tuna', 'fish', 'sandwich'], 159 | }, 160 | }, { modifier: true, upsert: true }).to.deep.equal(1); 161 | 162 | // Set or Array 163 | expectErrorLength(testSchema, { 164 | $setOnInsert: { 165 | allowedStringsSet: ['tuna', 'fish', 'sandwich'], 166 | }, 167 | }, { modifier: true, upsert: true }).to.deep.equal(1); 168 | 169 | // Array of objects 170 | expectErrorLength(friendsSchema, { 171 | $setOnInsert: { 172 | friends: [{ 173 | name: 'Bob', 174 | type: 'smelly', 175 | }], 176 | enemies: [], 177 | }, 178 | }, { modifier: true, upsert: true }).to.deep.equal(1); 179 | }); 180 | 181 | it('valid number', function () { 182 | expectErrorLength(testSchema, { 183 | $setOnInsert: { 184 | allowedNumbers: 1, 185 | }, 186 | }, { modifier: true, upsert: true }).to.deep.equal(0); 187 | 188 | // Array 189 | expectErrorLength(testSchema, { 190 | $setOnInsert: { 191 | allowedNumbersArray: [1, 2, 3], 192 | }, 193 | }, { modifier: true, upsert: true }).to.deep.equal(0); 194 | 195 | // Set or Array 196 | expectErrorLength(testSchema, { 197 | $setOnInsert: { 198 | allowedNumbersSet: [1, 2, 3], 199 | }, 200 | }, { modifier: true, upsert: true }).to.deep.equal(0); 201 | 202 | // Array of objects 203 | expectErrorLength(friendsSchema, { 204 | $setOnInsert: { 205 | friends: [{ 206 | name: 'Bob', 207 | type: 'best', 208 | a: { 209 | b: 5000, 210 | }, 211 | }], 212 | enemies: [], 213 | }, 214 | }, { modifier: true, upsert: true }).to.deep.equal(0); 215 | }); 216 | 217 | it('invalid number', function () { 218 | expectErrorLength(testSchema, { 219 | $setOnInsert: { 220 | allowedNumbers: 4, 221 | }, 222 | }, { modifier: true, upsert: true }).to.deep.equal(1); 223 | 224 | // Array 225 | expectErrorLength(testSchema, { 226 | $setOnInsert: { 227 | allowedNumbersArray: [1, 2, 3, 4], 228 | }, 229 | }, { modifier: true, upsert: true }).to.deep.equal(1); 230 | 231 | // Set or Array 232 | expectErrorLength(testSchema, { 233 | $setOnInsert: { 234 | allowedNumbersSet: [1, 2, 3, 4], 235 | }, 236 | }, { modifier: true, upsert: true }).to.deep.equal(1); 237 | 238 | // Array of objects 239 | expectErrorLength(friendsSchema, { 240 | $setOnInsert: { 241 | friends: [{ 242 | name: 'Bob', 243 | type: 'best', 244 | a: { 245 | b: 'wrong', 246 | }, 247 | }], 248 | enemies: [], 249 | }, 250 | }, { modifier: true, upsert: true }).to.deep.equal(1); 251 | }); 252 | }); 253 | 254 | describe('modifier with $set', function () { 255 | it('valid string', function () { 256 | expectErrorLength(testSchema, { 257 | $set: { 258 | allowedStrings: 'tuna', 259 | }, 260 | }, { modifier: true }).to.deep.equal(0); 261 | 262 | // Array 263 | expectErrorLength(testSchema, { 264 | $set: { 265 | allowedStringsArray: ['tuna', 'fish', 'salad'], 266 | }, 267 | }, { modifier: true }).to.deep.equal(0); 268 | 269 | // Set or Array 270 | expectErrorLength(testSchema, { 271 | $set: { 272 | allowedStringsSet: ['tuna', 'fish', 'salad'], 273 | }, 274 | }, { modifier: true }).to.deep.equal(0); 275 | 276 | // Array of objects 277 | expectErrorLength(friendsSchema, { 278 | $set: { 279 | 'friends.$.name': 'Bob', 280 | }, 281 | }, { modifier: true }).to.deep.equal(0); 282 | 283 | expectErrorLength(friendsSchema, { 284 | $set: { 285 | 'friends.1.name': 'Bob', 286 | }, 287 | }, { modifier: true }).to.deep.equal(0); 288 | }); 289 | 290 | it('invalid string', function () { 291 | expectErrorLength(testSchema, { 292 | $set: { 293 | allowedStrings: 'tunas', 294 | }, 295 | }, { modifier: true }).to.deep.equal(1); 296 | 297 | // Array 298 | expectErrorLength(testSchema, { 299 | $set: { 300 | allowedStringsArray: ['tuna', 'fish', 'sandwich'], 301 | }, 302 | }, { modifier: true }).to.deep.equal(1); 303 | 304 | // Set or Array 305 | expectErrorLength(testSchema, { 306 | $set: { 307 | allowedStringsSet: ['tuna', 'fish', 'sandwich'], 308 | }, 309 | }, { modifier: true }).to.deep.equal(1); 310 | 311 | // Array of objects 312 | expectErrorLength(friendsSchema, { 313 | $set: { 314 | 'friends.$.name': 'Bobby', 315 | }, 316 | }, { modifier: true }).to.deep.equal(1); 317 | 318 | expectErrorLength(friendsSchema, { 319 | $set: { 320 | 'friends.1.name': 'Bobby', 321 | }, 322 | }, { modifier: true }).to.deep.equal(1); 323 | }); 324 | 325 | it('valid number', function () { 326 | expectErrorLength(testSchema, { 327 | $set: { 328 | allowedNumbers: 1, 329 | }, 330 | }, { modifier: true }).to.deep.equal(0); 331 | 332 | expectErrorLength(testSchema, { 333 | $set: { 334 | allowedNumbersArray: [1, 2, 3], 335 | }, 336 | }, { modifier: true }).to.deep.equal(0); 337 | 338 | expectErrorLength(testSchema, { 339 | $set: { 340 | allowedNumbersSet: [1, 2, 3], 341 | }, 342 | }, { modifier: true }).to.deep.equal(0); 343 | }); 344 | 345 | it('invalid number', function () { 346 | expectErrorLength(testSchema, { 347 | $set: { 348 | allowedNumbers: 4, 349 | }, 350 | }, { modifier: true }).to.deep.equal(1); 351 | 352 | expectErrorLength(testSchema, { 353 | $set: { 354 | allowedNumbersArray: [1, 2, 3, 4], 355 | }, 356 | }, { modifier: true }).to.deep.equal(1); 357 | 358 | expectErrorLength(testSchema, { 359 | $set: { 360 | allowedNumbersSet: [1, 2, 3, 4], 361 | }, 362 | }, { modifier: true }).to.deep.equal(1); 363 | }); 364 | }); 365 | 366 | describe('getAllowedValuesForKey', function () { 367 | it('works', function () { 368 | const allowedValues = ['a', 'b']; 369 | const schema = new SimpleSchema({ 370 | foo: Array, 371 | 'foo.$': { 372 | type: String, 373 | allowedValues, 374 | }, 375 | }); 376 | expect(schema.getAllowedValuesForKey('foo')).to.deep.equal(allowedValues); 377 | }); 378 | 379 | it('works with set, convert to array', function () { 380 | const allowedValues = new Set(['a', 'b']); 381 | const schema = new SimpleSchema({ 382 | foo: Array, 383 | 'foo.$': { 384 | type: String, 385 | allowedValues, 386 | }, 387 | }); 388 | const fetchedAllowedValues = schema.getAllowedValuesForKey('foo'); 389 | expect(fetchedAllowedValues.includes('a')).to.be.ok; 390 | expect(fetchedAllowedValues.includes('b')).to.be.ok; 391 | expect(fetchedAllowedValues.length).to.deep.equal(2); 392 | }); 393 | 394 | it('returns null when allowedValues key is empty', function () { 395 | const schema = new SimpleSchema({ 396 | foo: Array, 397 | 'foo.$': { 398 | type: String, 399 | }, 400 | }); 401 | expect(schema.getAllowedValuesForKey('foo')).to.deep.equal(null); 402 | }); 403 | }); 404 | }); 405 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # aldeed:simple-schema CHANGELOG 2 | 3 | 4 | 5 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 6 | 7 | - [1.13.1](#1131) 8 | - [1.7.3 and higher](#173-and-higher) 9 | - [1.7.2](#172) 10 | - [1.7.1](#171) 11 | - [1.7.0](#170) 12 | - [1.6.2](#162) 13 | - [1.6.1](#161) 14 | - [1.6.0](#160) 15 | - [1.5.9](#159) 16 | - [1.5.8](#158) 17 | - [1.5.7](#157) 18 | - [1.5.6](#156) 19 | - [1.5.5](#155) 20 | - [1.5.4](#154) 21 | - [1.5.3](#153) 22 | - [1.5.2](#152) 23 | - [1.5.1](#151) 24 | - [1.5.0](#150) 25 | - [1.4.3](#143) 26 | - [1.4.2](#142) 27 | - [1.4.1](#141) 28 | - [1.4.0](#140) 29 | - [1.3.0](#130) 30 | - [1.2.2](#122) 31 | - [1.2.1](#121) 32 | - [1.2.0](#120) 33 | - [1.1.2](#112) 34 | - [1.1.1](#111) 35 | - [1.1.0](#110) 36 | - [1.0.0](#100) 37 | - [0.5.0](#050) 38 | - [0.4.2](#042) 39 | - [0.4.1](#041) 40 | - [0.4.0](#040) 41 | - [0.3.2](#032) 42 | - [0.3.1](#031) 43 | - [0.3.0](#030) 44 | - [0.2.3](#023) 45 | - [0.2.2](#022) 46 | - [0.2.1](#021) 47 | - [0.2.0](#020) 48 | - [0.1.1](#011) 49 | - [0.1.0](#010) 50 | - [0.0.4](#004) 51 | 52 | 53 | 54 | ## 1.13.1 55 | 56 | - back under community maintenance! 57 | - 100% compatibility with Meteor, used the last Meteor-compatible version from NPM 58 | - moved all code from npm simpl-schema@1.13.1 to this repo 59 | - NPM link: https://www.npmjs.com/package/simpl-schema/v/1.13.1 60 | - GitHub commit: [7f3ea1c2a185199e676726b6e4e82ab5fa722e97](https://github.com/longshotlabs/simpl-schema/tree/7f3ea1c2a185199e676726b6e4e82ab5fa722e97) 61 | - updated all tests to use `meteortesting:mocha` + chai 62 | - added coverage reporting to the tests! 63 | - prepare for Meteor 3.0 compatibility migration (will be done in the next version) 64 | - updated CI 65 | - publish as Meteor package 66 | 67 | ## 1.7.3 and higher 68 | 69 | Release notes for versions 1.7.3 and higher can be found at https://github.com/aldeed/simpl-schema/releases 70 | 71 | ## 1.7.2 72 | 73 | Update `message-box` dependency again to fix IE11 support 74 | 75 | ## 1.7.1 76 | 77 | Update `message-box` dependency to fix IE11 support 78 | 79 | ## 1.7.0 80 | 81 | If an array item (field ending with `.$`) has `optional: true`, this now allows it to have `null` items without any validation error being thrown. Previously adding `optional: true` to an array item had no effect. 82 | 83 | ## 1.6.2 84 | 85 | - Adds `SimpleSchema.regEx.idOfLength` for variable length IDs 86 | - Removes `deep-extend` dependency to fix undefined `Buffer` errors 87 | 88 | ## 1.6.1 89 | 90 | Omit test files from the published package 91 | 92 | ## 1.6.0 93 | 94 | - Removes all `lodash` packages 95 | - Replaces `extend` package with `deep-extend`, which is smaller and is the same package used by the `message-box` dependency 96 | - Improve the performance of handling fields with `blackbox: true` (thanks @cwouam) 97 | - Add a `this` context for all rule functions (see README) (thanks @Neobii) 98 | - Add a `this` context for all whole-doc validator functions (see README) (thanks @bhunjadi) 99 | 100 | ## 1.5.9 101 | 102 | Fix issues with autoValues not being available in other autoValues 103 | 104 | ## 1.5.8 105 | 106 | Update dependencies to fix vulnerabilities 107 | 108 | ## 1.5.7 109 | 110 | Update Babel config in an attempt to fully support IE11s 111 | 112 | ## 1.5.6 113 | 114 | - Update dependencies 115 | - Adjust the way Babel builds so that you don't need to do `.default` when importing in a non-Babel Node project. 116 | 117 | ## 1.5.5 118 | 119 | - Fix #294 - Now auto-converting values during cleaning does not convert if the value type is any of the types in a `oneOf` type 120 | 121 | ## 1.5.4 122 | 123 | - Add `$setOnInsert` to modifiers for defaultValues only when `isUpsert` is set to `true` in clean options or in extended autoValue context. It used to be ignored but newer MongoDB versions throw an error. Might fix #304 124 | - Fix #307 - Test for empty object when creating schema (thanks @coagmano) 125 | - autoValue functions sort preserves fields order on the same depth (thanks @bhunjadi) 126 | - `getAllowedValues` now returns `null` when `allowedValues` isn't set (thanks @MohammedEssehemy) 127 | - Update Mocha and other dependencies 128 | - Readme updates (thanks @ozzywalsh) 129 | 130 | ## 1.5.3 131 | 132 | Update to latest mongo-object package dependency 133 | 134 | ## 1.5.2 135 | 136 | Include README.md and LICENSE in the published package 137 | 138 | ## 1.5.1 139 | 140 | - Fix issues with `$pull` modifier being incorrectly cleaned in some cases where some properties have `defaultValue` (thanks @vparpoil) 141 | - Other behind-the-scenes refactoring 142 | 143 | ## 1.5.0 144 | 145 | - `allowedValues` may now be a `Set` instance (thanks @kevinkassimo) 146 | - Updated `EmailWithTLD` regular expression with one that is not susceptible to catastrophic backtracking attacks (thanks @davisjam) 147 | 148 | ## 1.4.3 149 | 150 | - Forgetting to define the parent key of any key in a schema will now throw an error 151 | - use Array.forEach to remove empty objects fixes #244 (#246) 152 | 153 | ## 1.4.2 154 | 155 | The SimpleSchema constructor or `.extend()` will now throw an error if you define an Array field but forget to define the corresponding array item field. 156 | 157 | ## 1.4.1 158 | 159 | Fixed an issue where defaultValues would be incorrectly added to `$setOnInsert` when your modifier contained `$unset` for deeply nested fields. 160 | 161 | ## 1.4.0 162 | 163 | - Fixed an issue where the defaultValue `$setOnInsert` added to a modifier containing `$addToSet` would target an incorrect object path. 164 | - When cleaning, it no longer tries to convert the type of `null`. 165 | - Any value returned from autoValue/defaultValue is now cloned to prevent accidental mutation. 166 | - Added `this.key` in the function context when executing schema definition properties that are functions. This can help you determine what the array index is for keys that are within arrays. 167 | - Added a `clone()` function on SimpleSchema instances. 168 | 169 | ## 1.3.0 170 | 171 | Add `this.key` and `this.closestSubschemaFieldName` to `autoValue` context to help with tricky situations when subschemas are used. 172 | 173 | ## 1.2.2 174 | 175 | Fix an issue introduced by 1.2.1, where it was possible for a SimpleSchema instance passed to `extend` to be mutated. 176 | 177 | ## 1.2.1 178 | 179 | Fix issues with Meteor Tracker reactivity sometimes not working when subschemas are involved. 180 | 181 | ## 1.2.0 182 | 183 | The performance of `clean`, specifically of looping through the object to apply autoValues and defaultValues, has been greatly improved for large objects. 184 | 185 | ## 1.1.2 186 | 187 | Passing a definition with no `type` to `extend` now works as expected, as long as the existing definition already has a `type`. 188 | 189 | ## 1.1.1 190 | 191 | Passing an array of schemas to `new SimpleSchema()` or `extend()` now throws an error rather than failing silently with strange results. 192 | 193 | ## 1.1.0 194 | 195 | - The `autoConvert` cleaning now converts strings that are "true" or "false" to Boolean if the schema expects a Boolean. 196 | - The `autoConvert` cleaning now converts numbers to Boolean if the schema expects a Boolean, with 0 being `false` and all other numbers being `true`. 197 | 198 | ## 1.0.0 199 | 200 | *BREAKING CHANGE:* autoValue and defaultValue handling has been rewritten to fix all known issues. As part of this rewrite, the behavior has changed to address a point of common confusion. 201 | 202 | Previously, when you cleaned an object to add autoValues, a `defaultValue` would be added (and an `autoValue` function would run) even if the parent object was not present. (It would be created.) 203 | 204 | Now, an `autoValue`/`defaultValue` will run only if the object in which it appears exists. Usually this is what you want, but if you are relying on the previous behavior, you can achieve the same thing by making sure that all ancestor objects have a `defaultValue: {}`. 205 | 206 | For example, this: 207 | 208 | ```js 209 | { 210 | profile: { 211 | type: Object, 212 | optional: true, 213 | }, 214 | 'profile.language': { 215 | type: String, 216 | defaultValue: 'en', 217 | }, 218 | } 219 | ``` 220 | 221 | previously cleaned `{}` to become `{ profile: { language: 'en' } }` but now would remain `{}`. If you want cleaning to result in `{ profile: { language: 'en' } }`, add the `profile` default value like: 222 | 223 | ```js 224 | { 225 | profile: { 226 | type: Object, 227 | optional: true, 228 | defaultValue: {}, 229 | }, 230 | 'profile.language': { 231 | type: String, 232 | defaultValue: 'en', 233 | }, 234 | } 235 | ``` 236 | 237 | If `profile` were nested under another object, you'd have to add `defaultValue: {}` to that object definition, too, and so on. 238 | 239 | - Fix regression that resulted in `_constructorOptions key is missing "type"` error reappearing in some situations 240 | - Fix errors when validating an object that has a property named `length` 241 | 242 | ## 0.5.0 243 | 244 | - Remove underscore dependency in favor of seperated lodash modules 245 | 246 | ## 0.4.2 247 | 248 | - Fix to properly add defaultValues in objects that are being $pushed in an update modifier 249 | - Fix removeNullsFromArrays to remove only nulls 250 | 251 | ## 0.4.1 252 | 253 | Fix cleaning an object with a `length` property 254 | 255 | ## 0.4.0 256 | 257 | - Added `getFormValidator()`, similar to `validator()` but instead of throwing an error, it returns a Promise that resolves with the errors. This can be used as a [Composable Form Specification validator](http://forms.dairystatedesigns.com/user/validation/). 258 | - Throw a better error when keys that conflict with Object prototype keys are used (Thanks @xavierpriour) 259 | - Fix the incorrect "Found both autoValue and defaultValue options" warning (Thanks @SachaG) 260 | 261 | ## 0.3.2 262 | 263 | Bump dependencies to fix `messages` issues 264 | 265 | ## 0.3.1 266 | 267 | - When calling `pick` or `omit`, the `messageBox` and all original `SimpleSchema` constructor options are now properly kept. (Thanks @plumpudding) 268 | - Fixed #80 (Thanks @jasonphillips) 269 | - `getQuickTypeForKey` may now return additional strings "object" or "objectArray" 270 | - Fix erroneous "Found both autoValue and defaultValue" warning (Thanks @SachaG) 271 | - Fix passing of clean options when extending 272 | - Other fixes to extending logic 273 | 274 | ## 0.3.0 275 | 276 | - Added human-friendly `message` to each validation error in the `details` array on a thrown ClientError (thanks @unknown4unnamed) 277 | - Fixed isInteger error on IE11 (thanks @lmachens) 278 | - Switched to duck typing for `SimpleSchema` instanceof checks to fix failures due to multiple instances of the package (thanks @dpankros) 279 | - Fixed multiple calls to `messages` for different schemas from affecting the other schemas (thanks @Josh-ES) 280 | 281 | ## 0.2.3 282 | 283 | - Add missing deep-extend dependency 284 | 285 | ## 0.2.2 286 | 287 | - Fixed Meteor Tracker reactivity 288 | 289 | ## 0.2.1 290 | 291 | - It is no longer considered a validation error when a key within $unset is not defined in the schema. 292 | 293 | ## 0.2.0 294 | 295 | - Added `ssInstance.getQuickTypeForKey(key)` 296 | - Added `ssInstance.getObjectSchema(key)` 297 | 298 | ## 0.1.1 299 | 300 | - Improved error for missing `type` property 301 | - Use _.contains instead of Array.includes to fix some compatibility issues (thanks @DerekTBrown) 302 | - Various documentation and test fixes 303 | 304 | ## 0.1.0 305 | 306 | - Added `ssInstance.getAllowedValuesForKey(key)` 307 | 308 | ## 0.0.4 309 | 310 | - Removed the `babel-polyfill` dependency. It may not cause problems, but to be safe you'll want to be sure that your app depends on and imports `babel-polyfill` or some other ES2015 polyfill package. 311 | - `this.validationContext` is now available in all custom validator functions (thanks @yanickrochon) 312 | - You can now call `SimpleSchema.setDefaultMessages(messages)`, passing in the same object you would pass to the `MessageBox` constructor, if you want to override the default messages for all schemas. This is in addition to being able to set `schema.messageBox` to your own custom `MessageBox` instance for a single schema, which you could already do. (thanks @clayne11) 313 | - Labels with certain characters like single quotes will now show up correctly in validation error messages. (thanks @clayne11) 314 | - `extend` is now chainable 315 | - Requiredness validation now works for required fields that are in subschemas 316 | - Fixed some issues with autoValues not being correctly added when they were deeply nested under several levels of arrays and objects. 317 | -------------------------------------------------------------------------------- /lib/SimpleSchema_regEx.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names, prefer-arrow-callback */ 2 | 3 | import { expect } from 'chai'; 4 | import { SimpleSchema } from './SimpleSchema'; 5 | 6 | describe('SimpleSchema', function () { 7 | it('regEx - issue 409', function () { 8 | // Make sure no regEx errors for optional 9 | const schema = new SimpleSchema({ 10 | foo: { 11 | type: String, 12 | optional: true, 13 | regEx: /bar/, 14 | }, 15 | }); 16 | 17 | expect(schema.newContext().validate({})).to.deep.equal(true); 18 | expect(schema.newContext().validate({ foo: null })).to.deep.equal(true); 19 | expect(schema.newContext().validate({ foo: '' })).to.deep.equal(false); 20 | }); 21 | 22 | it('no regEx errors for empty strings when `skipRegExCheckForEmptyStrings` field option is true', function () { 23 | const schema = new SimpleSchema({ 24 | foo: { 25 | type: String, 26 | optional: true, 27 | regEx: /bar/, 28 | skipRegExCheckForEmptyStrings: true, 29 | }, 30 | }); 31 | 32 | expect(schema.newContext().validate({ foo: '' })).to.equal(true); 33 | 34 | // still fails when not empty string, though 35 | expect(schema.newContext().validate({ foo: 'bad' })).to.equal(false); 36 | }); 37 | 38 | it('Built-In RegEx and Messages', function () { 39 | const schema = new SimpleSchema({ 40 | email: { 41 | type: String, 42 | regEx: SimpleSchema.RegEx.Email, 43 | optional: true, 44 | }, 45 | emailWithTLD: { 46 | type: String, 47 | regEx: SimpleSchema.RegEx.EmailWithTLD, 48 | optional: true, 49 | }, 50 | domain: { 51 | type: String, 52 | regEx: SimpleSchema.RegEx.Domain, 53 | optional: true, 54 | }, 55 | weakDomain: { 56 | type: String, 57 | regEx: SimpleSchema.RegEx.WeakDomain, 58 | optional: true, 59 | }, 60 | ip: { 61 | type: String, 62 | regEx: SimpleSchema.RegEx.IP, 63 | optional: true, 64 | }, 65 | ip4: { 66 | type: String, 67 | regEx: SimpleSchema.RegEx.IPv4, 68 | optional: true, 69 | }, 70 | ip6: { 71 | type: String, 72 | regEx: SimpleSchema.RegEx.IPv6, 73 | optional: true, 74 | }, 75 | url: { 76 | type: String, 77 | regEx: SimpleSchema.RegEx.Url, 78 | optional: true, 79 | }, 80 | id: { 81 | type: String, 82 | regEx: SimpleSchema.RegEx.Id, 83 | optional: true, 84 | }, 85 | longId: { 86 | type: String, 87 | regEx: SimpleSchema.RegEx.idOfLength(32), 88 | optional: true, 89 | }, 90 | }); 91 | 92 | const c1 = schema.newContext(); 93 | c1.validate({ 94 | email: 'foo', 95 | }); 96 | expect(c1.validationErrors().length).to.deep.equal(1); 97 | expect(c1.keyErrorMessage('email')).to.deep.equal('Email must be a valid email address'); 98 | 99 | c1.validate({ 100 | emailWithTLD: 'foo', 101 | }); 102 | expect(c1.validationErrors().length).to.deep.equal(1); 103 | expect(c1.keyErrorMessage('emailWithTLD')).to.deep.equal('Email with tld must be a valid email address'); 104 | 105 | c1.validate({ 106 | domain: 'foo', 107 | }); 108 | expect(c1.validationErrors().length).to.deep.equal(1); 109 | expect(c1.keyErrorMessage('domain')).to.deep.equal('Domain must be a valid domain'); 110 | 111 | c1.validate({ 112 | weakDomain: '///jioh779&%', 113 | }); 114 | expect(c1.validationErrors().length).to.deep.equal(1); 115 | expect(c1.keyErrorMessage('weakDomain')).to.deep.equal('Weak domain must be a valid domain'); 116 | 117 | c1.validate({ 118 | ip: 'foo', 119 | }); 120 | expect(c1.validationErrors().length).to.deep.equal(1); 121 | expect(c1.keyErrorMessage('ip')).to.deep.equal('Ip must be a valid IPv4 or IPv6 address'); 122 | 123 | c1.validate({ 124 | ip4: 'foo', 125 | }); 126 | expect(c1.validationErrors().length).to.deep.equal(1); 127 | expect(c1.keyErrorMessage('ip4')).to.deep.equal('Ip4 must be a valid IPv4 address'); 128 | 129 | c1.validate({ 130 | ip6: 'foo', 131 | }); 132 | expect(c1.validationErrors().length).to.deep.equal(1); 133 | expect(c1.keyErrorMessage('ip6')).to.deep.equal('Ip6 must be a valid IPv6 address'); 134 | 135 | c1.validate({ 136 | url: 'foo', 137 | }); 138 | expect(c1.validationErrors().length).to.deep.equal(1); 139 | expect(c1.keyErrorMessage('url')).to.deep.equal('Url must be a valid URL'); 140 | 141 | c1.validate({ 142 | id: '%#$%', 143 | }); 144 | expect(c1.validationErrors().length).to.deep.equal(1); 145 | expect(c1.keyErrorMessage('id')).to.deep.equal('ID must be a valid alphanumeric ID'); 146 | 147 | c1.validate({ 148 | longId: '%#$%', 149 | }); 150 | expect(c1.validationErrors().length).to.deep.equal(1); 151 | expect(c1.keyErrorMessage('longId')).to.deep.equal('Long ID failed regular expression validation'); 152 | }); 153 | 154 | it('Optional regEx in subobject', function () { 155 | const schema = new SimpleSchema({ 156 | foo: { 157 | type: Object, 158 | optional: true, 159 | }, 160 | 'foo.url': { 161 | type: String, 162 | regEx: SimpleSchema.RegEx.Url, 163 | optional: true, 164 | }, 165 | }); 166 | 167 | const context = schema.namedContext(); 168 | 169 | expect(context.validate({})).to.deep.equal(true); 170 | 171 | expect(context.validate({ 172 | foo: {}, 173 | })).to.deep.equal(true); 174 | 175 | expect(context.validate({ 176 | foo: { 177 | url: null, 178 | }, 179 | })).to.deep.equal(true); 180 | 181 | expect(context.validate({ 182 | $set: { 183 | foo: {}, 184 | }, 185 | }, { modifier: true })).to.deep.equal(true); 186 | 187 | expect(context.validate({ 188 | $set: { 189 | 'foo.url': null, 190 | }, 191 | }, { modifier: true })).to.deep.equal(true); 192 | 193 | expect(context.validate({ 194 | $unset: { 195 | 'foo.url': '', 196 | }, 197 | }, { modifier: true })).to.deep.equal(true); 198 | }); 199 | 200 | it('SimpleSchema.RegEx.Email', function () { 201 | const expr = SimpleSchema.RegEx.Email; 202 | 203 | function isTrue(s) { 204 | expect(expr.test(s)).to.equal(true); 205 | } 206 | 207 | function isFalse(s) { 208 | expect(expr.test(s)).to.equal(false); 209 | } 210 | 211 | isTrue('name@web.de'); 212 | isTrue('name+addition@web.de'); 213 | isTrue('st#r~ange.e+mail@web.de'); 214 | isTrue('name@localhost'); 215 | isTrue('name@192.168.200.5'); 216 | isFalse('name@BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 217 | isFalse('name@BCDF:45AB:1245:75B9::0987:1234:1324'); 218 | isFalse('name@BCDF:45AB:1245:75B9:0987:1234:1324'); 219 | isFalse('name@::1'); 220 | }); 221 | 222 | it('SimpleSchema.RegEx.EmailWithTLD', function () { 223 | const expr = SimpleSchema.RegEx.EmailWithTLD; 224 | 225 | function isTrue(s) { 226 | expect(expr.test(s)).to.equal(true); 227 | } 228 | 229 | function isFalse(s) { 230 | expect(expr.test(s)).to.equal(false); 231 | } 232 | 233 | isTrue('name@web.de'); 234 | isTrue('name+addition@web.de'); 235 | isTrue('st#r~ange.e+mail@web.de'); 236 | isFalse('name@localhost'); 237 | isFalse('name@192.168.200.5'); 238 | isFalse('name@BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 239 | isFalse('name@BCDF:45AB:1245:75B9::0987:1234:1324'); 240 | isFalse('name@BCDF:45AB:1245:75B9:0987:1234:1324'); 241 | isFalse('name@::1'); 242 | }); 243 | 244 | it('SimpleSchema.RegEx.Domain', function () { 245 | const expr = SimpleSchema.RegEx.Domain; 246 | 247 | function isTrue(s) { 248 | expect(expr.test(s)).to.equal(true); 249 | } 250 | 251 | function isFalse(s) { 252 | expect(expr.test(s)).to.equal(false); 253 | } 254 | 255 | isTrue('domain.com'); 256 | isFalse('localhost'); 257 | isFalse('192.168.200.5'); 258 | isFalse('BCDF:45AB:1245:75B9:0987:1562:4567:1234:AB36'); 259 | }); 260 | 261 | it('SimpleSchema.RegEx.WeakDomain', function () { 262 | const expr = SimpleSchema.RegEx.WeakDomain; 263 | 264 | function isTrue(s) { 265 | expect(expr.test(s)).to.equal(true); 266 | } 267 | 268 | isTrue('domain.com'); 269 | isTrue('localhost'); 270 | isTrue('192.168.200.5'); 271 | isTrue('BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 272 | }); 273 | 274 | it('SimpleSchema.RegEx.IP', function () { 275 | const expr = SimpleSchema.RegEx.IP; 276 | 277 | function isTrue(s) { 278 | expect(expr.test(s)).to.equal(true); 279 | } 280 | 281 | function isFalse(s) { 282 | expect(expr.test(s)).to.equal(false); 283 | } 284 | 285 | isFalse('localhost'); 286 | isTrue('192.168.200.5'); 287 | isFalse('320.168.200.5'); 288 | isFalse('192.168.5'); 289 | isTrue('BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 290 | isFalse('BCDF:45AB:1245:75B9:0987:1562:4567:1234:AB36'); 291 | isTrue('BCDF:45AB:1245:75B9::0987:1234:1324'); 292 | isFalse('BCDF:45AB:1245:75B9:0987:1234:1324'); 293 | isTrue('::1'); 294 | }); 295 | 296 | it('SimpleSchema.RegEx.IPv4', function () { 297 | const expr = SimpleSchema.RegEx.IPv4; 298 | 299 | function isTrue(s) { 300 | expect(expr.test(s)).to.equal(true); 301 | } 302 | 303 | function isFalse(s) { 304 | expect(expr.test(s)).to.equal(false); 305 | } 306 | 307 | isFalse('localhost'); 308 | isTrue('192.168.200.5'); 309 | isFalse('320.168.200.5'); 310 | isFalse('192.168.5'); 311 | isFalse('BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 312 | isFalse('BCDF:45AB:1245:75B9:0987:1562:4567:1234:AB36'); 313 | isFalse('BCDF:45AB:1245:75B9::0987:1234:1324'); 314 | isFalse('BCDF:45AB:1245:75B9:0987:1234:1324'); 315 | isFalse('::1'); 316 | }); 317 | 318 | it('SimpleSchema.RegEx.IPv6', function () { 319 | const expr = SimpleSchema.RegEx.IPv6; 320 | 321 | function isTrue(s) { 322 | expect(expr.test(s)).to.equal(true); 323 | } 324 | 325 | function isFalse(s) { 326 | expect(expr.test(s)).to.equal(false); 327 | } 328 | 329 | isFalse('localhost'); 330 | isFalse('192.168.200.5'); 331 | isFalse('320.168.200.5'); 332 | isFalse('192.168.5'); 333 | isTrue('BCDF:45AB:1245:75B9:0987:1562:4567:1234'); 334 | isFalse('BCDF:45AB:1245:75B9:0987:1562:4567:1234:AB36'); 335 | isTrue('BCDF:45AB:1245:75B9::0987:1234:1324'); 336 | isFalse('BCDF:45AB:1245:75B9:0987:1234:1324'); 337 | isTrue('::1'); 338 | }); 339 | 340 | // this is a simple fake-random id generator that generates the 341 | // ids with numbers that are expected to be valid for the Id and IdOf regexp 342 | const Random = { 343 | UNMISTAKABLE_CHARS: '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz', 344 | id (charsCount = 17) { 345 | let id = ''; 346 | const len = Random.UNMISTAKABLE_CHARS.length; 347 | for (let i = 0; i < charsCount; i++) { 348 | const index = Math.floor(Math.random() * len); 349 | id += Random.UNMISTAKABLE_CHARS[index]; 350 | } 351 | return id; 352 | }, 353 | }; 354 | 355 | it('SimpleSchema.RegEx.Id', function () { 356 | const idExpr = SimpleSchema.RegEx.Id; 357 | const isTrue = (s) => expect(idExpr.test(s)).to.equal(true); 358 | const isFalse = (s) => expect(idExpr.test(s)).to.equal(false); 359 | 360 | isTrue(Random.id()); 361 | isFalse(Random.id(16)); // less 362 | isFalse(Random.id(18)); // greater 363 | isFalse('01234567891011123'); // invalid chars 364 | }); 365 | 366 | it('SimpleSchema.RegEx.idOfLength', function () { 367 | const { idOfLength } = SimpleSchema.RegEx; 368 | const expectThrows = (min, max) => expect(() => idOfLength(min, max)).to.throw(/Expected a non-negative safe integer/); 369 | 370 | // lets add some fuzzing to see if there are some unexpected edge cases 371 | // when generating the id RegExp pattern using SimpleSchema.RegEx.IdOf 372 | const randomMinValues = (fn, times) => (new Array(times)).forEach(() => expectThrows(fn())); 373 | const randomMaxValues = (min, fn, times) => (new Array(times)).forEach(() => expectThrows(min, fn())); 374 | 375 | // unexpected min values 376 | 377 | // no negatives 378 | randomMinValues(() => -1 * Math.floor(Math.random() * 100), 100); 379 | // no floating point numbers 380 | randomMinValues(() => Math.random(), 100); 381 | // only Number.MAX_SAFE_INTEGER 382 | expectThrows(9007199254740992); 383 | 384 | // unexpected max values 385 | 386 | // not less than min 387 | expectThrows(10, 9); 388 | // no negatives 389 | randomMaxValues(10, () => -1 * Math.floor(Math.random() * 100), 100); 390 | // no negatives 391 | randomMaxValues(10, () => -1 * Math.floor(Math.random() * 100), 100); 392 | // no floating point numbers 393 | randomMaxValues(10, () => Math.random(), 100); 394 | // only Number.MAX_SAFE_INTEGER 395 | expectThrows(10, 9007199254740992); 396 | 397 | const isTrue = (expr, s) => expect(expr.test(s)).to.equal(true); 398 | const isFalse = (expr, s) => expect(expr.test(s)).to.equal(false); 399 | 400 | // arbitrary length ids 401 | const anyLen = idOfLength(0, null); 402 | for (let i = 1; i < 100; i++) { 403 | isTrue(anyLen, Random.id(i)); 404 | } 405 | 406 | // fixed length ids 407 | isTrue(idOfLength(17), Random.id()); 408 | isTrue(idOfLength(32), Random.id(32)); 409 | isFalse(idOfLength(16), Random.id()); // greater 410 | isFalse(idOfLength(32), Random.id()); // less 411 | 412 | // range of length ids with fixed upper bound 413 | isTrue(idOfLength(8, 128), '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'); 414 | isFalse(idOfLength(8, 128), '1234567890abcdefghijklmnopqrstuvwxyz'); // invalid chars 415 | isFalse(idOfLength(8, 128), '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz%$/(='); // invalid chars 2 416 | 417 | // range of length ids with arbitrary upper bound 418 | isTrue(idOfLength(8, null), '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'); 419 | isFalse(idOfLength(8, null), '1234567890abcdefghijklmnopqrstuvwxyz'); // invalid chars 420 | isFalse(idOfLength(8, null), '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz%$/(='); // invalid chars 2 421 | }); 422 | }); 423 | --------------------------------------------------------------------------------