├── .eslintignore ├── .gitignore ├── lib ├── StructureError.js ├── ValidationError.js ├── index.js ├── helpers.js ├── Schema.js ├── ModelProxy.js ├── validators.js ├── Field.js ├── filters.js └── Model.js ├── jest.config.js ├── license.txt ├── package.json ├── test ├── utils.js └── validators.test.js ├── .eslintrc.json ├── README.md └── DOCS.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /playground -------------------------------------------------------------------------------- /lib/StructureError.js: -------------------------------------------------------------------------------- 1 | class StructureError extends Error {} 2 | 3 | module.exports = StructureError 4 | -------------------------------------------------------------------------------- /lib/ValidationError.js: -------------------------------------------------------------------------------- 1 | class ValidationError extends Error {} 2 | 3 | module.exports = ValidationError 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRegex: '\\.(test)\\.(js)$', 3 | testPathIgnorePatterns: ['lib/', 'node_modules/'], 4 | moduleFileExtensions: ['js'], 5 | testEnvironment: 'node', 6 | rootDir: 'test', 7 | } 8 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Field = require('./Field') 2 | const Schema = require('./Schema') 3 | const Model = require('./Model') 4 | const StructureError = require('./StructureError') 5 | const ValidationError = require('./ValidationError') 6 | 7 | module.exports = { 8 | field(...args) { 9 | return new Field(...args) 10 | }, 11 | schema(...args) { 12 | return new Schema(...args) 13 | }, 14 | Field, 15 | Schema, 16 | Model, 17 | StructureError, 18 | ValidationError, 19 | } 20 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Patryk Pawłowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Patryk Pawłowski", 3 | "name": "firestore-schema-validator", 4 | "description": "Interface for creating models, schemas and validate data for Google Cloud Firestore.", 5 | "version": "0.8.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bypatryk/firestore-schema-validator.git" 9 | }, 10 | "keywords": [ 11 | "schema", 12 | "model", 13 | "validation", 14 | "google", 15 | "cloud", 16 | "firestore", 17 | "odm", 18 | "orm" 19 | ], 20 | "main": "lib/index.js", 21 | "files": [ 22 | "lib" 23 | ], 24 | "scripts": { 25 | "lint": "eslint .", 26 | "test": "jest", 27 | "docs": "jsdoc2md lib/*.js > DOCS.md" 28 | }, 29 | "engines": { 30 | "node": ">=8.0.0" 31 | }, 32 | "license": "MIT", 33 | "devDependencies": { 34 | "babel-eslint": "^10.0.1", 35 | "eslint": "^5.16.0", 36 | "eslint-plugin-promise": "^4.1.1", 37 | "firebase-admin": "^7.3.0", 38 | "jest": "^24.8.0", 39 | "jsdoc": "^3.6.2", 40 | "jsdoc-to-markdown": "^5.0.0" 41 | }, 42 | "dependencies": { 43 | "moment": "^2.24.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs async code for each item in array. 3 | * 4 | * @param {Array} array Array of items. 5 | * @param {Function} callback Callback for each item. 6 | */ 7 | const asyncForEach = async (array, callback) => { 8 | /* eslint no-await-in-loop: 0 */ 9 | for (const index in array) { 10 | await callback(array[index], index, array) 11 | } 12 | } 13 | 14 | /** 15 | * Checks if value is an Object. 16 | * 17 | * @param {*} value 18 | * @returns {Boolean} Boolean flag. 19 | * @memberof helpers 20 | */ 21 | const isObject = (value) => 22 | typeof value === 'object' && value !== null 23 | 24 | /** 25 | * Marks (part of) Document Data as changed. 26 | * 27 | * @param {Set} changedKeys Set with paths of changed keys. 28 | * @param {*} value (Part of) Document Data 29 | * @param {Array} path Path for recurring calls. 30 | * @returns {Boolean} Boolean flag. 31 | * @memberof helpers 32 | */ 33 | const markAsChanged = (changedKeys, value, path = []) => { 34 | for (let i = 0; i < path.length; i++) { 35 | const subPath = path.slice(0, i + 1) 36 | 37 | changedKeys.add(subPath.join('.')) 38 | } 39 | 40 | if (isObject(value)) 41 | Object.keys(value) 42 | .forEach(key => markAsChanged(changedKeys, value[key], [ ...path, key ])) 43 | } 44 | 45 | module.exports = { 46 | asyncForEach, 47 | isObject, 48 | markAsChanged, 49 | } -------------------------------------------------------------------------------- /lib/Schema.js: -------------------------------------------------------------------------------- 1 | const { asyncForEach } = require('./helpers') 2 | 3 | /** 4 | * Definition of Document Schema. 5 | * 6 | * @class Schema 7 | */ 8 | class Schema { 9 | /** 10 | * Creates an instance of Schema. 11 | * 12 | * @param {Object} _fields Object containing Field definitions. 13 | * @memberof Schema 14 | */ 15 | constructor (_fields) { 16 | this._fields = _fields 17 | } 18 | 19 | /** 20 | * Validates Document Data agains Fields. 21 | * 22 | * @param {Object} [data={}] 23 | * @param {Object} [fields=this._fields] 24 | * @returns Validated Document Data. 25 | * @memberof Schema 26 | */ 27 | async validate(data = {}, fields = this._fields) { 28 | const filteredData = {} 29 | 30 | await asyncForEach( 31 | Object.entries(fields), 32 | async ([key, field]) => { 33 | const filteredFieldData = await field.validate(data[key]) 34 | 35 | if (filteredFieldData !== field._OptionalSymbol) 36 | filteredData[key] = filteredFieldData 37 | } 38 | ) 39 | 40 | return filteredData 41 | } 42 | 43 | /** 44 | * Validates Document Data against selected Fields. 45 | * 46 | * @param {Object} [data={}] Document Data 47 | * @param {Set} [changedKeys=new Set()] Set with Paths of changed Fields. 48 | * @returns Valdiated Document Data. 49 | * @memberof Schema 50 | */ 51 | async validateSelected(data = {}, changedKeys = new Set()) { 52 | const selectedFields = {} 53 | 54 | for (let key of changedKeys.keys()) { 55 | key = key.split('.')[0] 56 | selectedFields[key] = this._fields[key] 57 | } 58 | 59 | return { 60 | ...data, 61 | ...await this.validate(data, selectedFields) 62 | } 63 | } 64 | } 65 | 66 | module.exports = Schema 67 | -------------------------------------------------------------------------------- /lib/ModelProxy.js: -------------------------------------------------------------------------------- 1 | const Field = require('./Field') 2 | const { isObject, markAsChanged } = require('./helpers') 3 | 4 | const isPrivate = (key) => { 5 | return key[0] === '_' 6 | } 7 | 8 | const isData = (target, key) => { 9 | const fields = target.constructor._schema._fields 10 | 11 | return fields.hasOwnProperty(key) 12 | && fields[key] instanceof Field 13 | } 14 | 15 | const nestedHandler = (model, path = []) => { 16 | return { 17 | get(target, key) { 18 | if (isObject(target[key])) 19 | return new Proxy( 20 | target[key], 21 | nestedHandler(model, [ 22 | ...path, 23 | key, 24 | ]) 25 | ) 26 | 27 | return target[key] 28 | }, 29 | 30 | set(target, key, value) { 31 | markAsChanged(model._changedKeys, value, [ ...path, key ]) 32 | return Reflect.set(target, key, value) 33 | } 34 | } 35 | } 36 | 37 | const ModelProxy = function (model) { 38 | return new Proxy(model, { 39 | get(target, key) { 40 | if (isPrivate(key)) 41 | return target[key] 42 | 43 | if (isData(target, key) && isObject(target._data[key])) 44 | return new Proxy(target._data[key], nestedHandler(target, [key])) 45 | 46 | if (isData(target, key)) 47 | return target._data[key] 48 | 49 | return target[key] 50 | }, 51 | 52 | set(target, key, value) { 53 | if (isPrivate(key)) 54 | return Reflect.set(target, key, value) 55 | 56 | if (isData(target, key)) { 57 | markAsChanged(target._changedKeys, value, [ key ]) 58 | target._data[key] = value 59 | 60 | return Reflect.set(target._data, key, value) 61 | } 62 | 63 | return Reflect.set(target, key, value) 64 | }, 65 | }) 66 | } 67 | 68 | module.exports = ModelProxy 69 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | const moment = require('moment') 3 | 4 | const FIRESTORE_TYPES = [ 5 | 'Array', 6 | 'Map', 7 | 'Boolean', 8 | 'Number', 9 | 'DocumentReference', 10 | 'GeoPoint', 11 | 'String', 12 | 'Timestamp', 13 | ] 14 | 15 | const isAfter = (date, value) => 16 | moment(date).diff(value) < 0 17 | 18 | const isAny = value => 19 | isArray(value) 20 | || isBoolean(value) 21 | || isDocumentReference(value) 22 | || isGeoPoint(value) 23 | || isMap(value) 24 | || isNumber(value) 25 | || isString(value) 26 | || isTimestamp(value) 27 | || isNull(value) 28 | 29 | const isArray = value => 30 | Array.isArray(value) 31 | 32 | const isBefore = (date, value) => 33 | moment(value).diff(date) < 0 34 | 35 | const isBoolean = value => 36 | typeof value === 'boolean' 37 | 38 | const isDocumentReference = value => 39 | value instanceof admin.firestore.DocumentReference 40 | 41 | const isGeoPoint = value => 42 | value instanceof admin.firestore.GeoPoint 43 | 44 | const isInRange = (min, max, value) => 45 | min <= value 46 | && value <= max 47 | 48 | const isInteger = value => 49 | isNumber(value) && Number.isInteger(value) 50 | 51 | const isMap = value => 52 | typeof value === 'object' 53 | && value !== null 54 | && value.constructor instanceof value.constructor 55 | 56 | const isMatching = (regex, value) => 57 | regex.test(value) 58 | 59 | const isNull = value => 60 | value === null 61 | 62 | const isNumber = value => 63 | typeof value === 'number' 64 | && !isNaN(value) 65 | 66 | const isString = value => 67 | typeof value === 'string' 68 | 69 | const isTimestamp = value => 70 | value instanceof admin.firestore.Timestamp 71 | 72 | module.exports = { 73 | isAfter, 74 | isAny, 75 | isArray, 76 | isBefore, 77 | isBoolean, 78 | isDocumentReference, 79 | isGeoPoint, 80 | isInRange, 81 | isInteger, 82 | isMap, 83 | isMatching, 84 | isNull, 85 | isNumber, 86 | isString, 87 | isTimestamp, 88 | FIRESTORE_TYPES, 89 | } 90 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | 3 | const arrays = [ 4 | [], 5 | [1, '2', true, null], 6 | ] 7 | 8 | const booleans = [ false, true ] 9 | 10 | const documentReferences = [ 11 | new admin.firestore.DocumentReference(), 12 | ] 13 | 14 | const geoPoints = [ 15 | new admin.firestore.GeoPoint(-50, 100), 16 | ] 17 | 18 | const integers = [ 19 | 1, 20 | 0, 21 | 1.0, 22 | ] 23 | 24 | const maps = [ 25 | {}, 26 | { 0: 'a' }, 27 | new Object({}), 28 | ] 29 | 30 | const nulls = [ 31 | null, 32 | ] 33 | 34 | const numbers = [ 35 | -Infinity, 36 | Infinity, 37 | Math.PI, 38 | 1.05, 39 | ] 40 | 41 | const strings = [ 42 | '', 43 | 'abc', 44 | '1', 45 | '0', 46 | ` 47 | multiline 48 | string 49 | `, 50 | ] 51 | 52 | const timestamps = [ 53 | new admin.firestore.Timestamp(10000, 10000), 54 | ] 55 | 56 | const acceptable = [ 57 | ...arrays, 58 | ...booleans, 59 | ...documentReferences, 60 | ...geoPoints, 61 | ...integers, 62 | ...maps, 63 | ...nulls, 64 | ...numbers, 65 | ...strings, 66 | ...timestamps, 67 | ] 68 | 69 | const nonAcceptable = [ 70 | undefined, 71 | NaN, 72 | () => {}, 73 | Symbol(), 74 | new ArrayBuffer(10), 75 | new Boolean(true), 76 | new Object(true), 77 | new Object(10), 78 | new String('string'), 79 | new Object('string'), 80 | new Date(), 81 | new Error('Error'), 82 | new Map(), 83 | new WeakMap(), 84 | new Set(), 85 | new WeakSet(), 86 | new RegExp(), 87 | new Promise(resolve => resolve), 88 | new class SomeClass {}, 89 | ] 90 | 91 | const all = [ 92 | ...acceptable, 93 | ...nonAcceptable, 94 | ] 95 | 96 | const allExcept = (except) => all.filter(item => !except.includes(item)) 97 | 98 | module.exports = { 99 | all, 100 | allExcept, 101 | arrays, 102 | acceptable, 103 | nonAcceptable, 104 | booleans, 105 | documentReferences, 106 | geoPoints, 107 | integers, 108 | maps, 109 | numbers, 110 | strings, 111 | timestamps, 112 | } 113 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | // Required for certain syntax usages 5 | "ecmaVersion": 2017 6 | }, 7 | "env": { 8 | "commonjs": true, 9 | "es6": true, 10 | "node": true, 11 | "jest": true 12 | }, 13 | "plugins": [ 14 | "promise" 15 | ], 16 | "extends": "eslint:recommended", 17 | "rules": { 18 | "indent": [ 19 | "error", 20 | 2, 21 | { 22 | "SwitchCase": 1 23 | } 24 | ], 25 | 26 | "linebreak-style": [ 27 | "error", 28 | "unix" 29 | ], 30 | 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | 36 | "semi": [ 37 | "error", 38 | "never" 39 | ], 40 | 41 | "no-unused-vars": ["error", { 42 | "vars": "all", 43 | "args": "none", 44 | "ignoreRestSiblings": false 45 | }], 46 | 47 | // Removed rule "disallow the use of console" from recommended eslint rules 48 | "no-console": "off", 49 | 50 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 51 | "no-regex-spaces": "off", 52 | 53 | // Removed rule "disallow the use of debugger" from recommended eslint rules 54 | "no-debugger": "off", 55 | 56 | // Warn against template literal placeholder syntax in regular strings 57 | "no-template-curly-in-string": 1, 58 | 59 | // Warn if return statements do not either always or never specify values 60 | "consistent-return": 1, 61 | 62 | // Warn if no return statements in callbacks of array methods 63 | "array-callback-return": 1, 64 | 65 | // Require the use of === and !== 66 | "eqeqeq": 2, 67 | 68 | // Disallow the use of alert, confirm, and prompt 69 | "no-alert": 2, 70 | 71 | // Disallow the use of arguments.caller or arguments.callee 72 | "no-caller": 2, 73 | 74 | // Disallow null comparisons without type-checking operators 75 | "no-eq-null": 2, 76 | 77 | // Disallow the use of eval() 78 | "no-eval": 2, 79 | 80 | // Warn against extending native types 81 | "no-extend-native": 1, 82 | 83 | // Warn against unnecessary calls to .bind() 84 | "no-extra-bind": 1, 85 | 86 | // Warn against unnecessary labels 87 | "no-extra-label": 1, 88 | 89 | // Disallow leading or trailing decimal points in numeric literals 90 | "no-floating-decimal": 2, 91 | 92 | // Warn against shorthand type conversions 93 | "no-implicit-coercion": 1, 94 | 95 | // Warn against function declarations and expressions inside loop statements 96 | "no-loop-func": 1, 97 | 98 | // Disallow new operators with the Function object 99 | "no-new-func": 2, 100 | 101 | // Warn against new operators with the String, Number, and Boolean objects 102 | "no-new-wrappers": 1, 103 | 104 | // Disallow throwing literals as exceptions 105 | "no-throw-literal": 2, 106 | 107 | // Require using Error objects as Promise rejection reasons 108 | "prefer-promise-reject-errors": 2, 109 | 110 | // Enforce “for” loop update clause moving the counter in the right direction 111 | "for-direction": 2, 112 | 113 | // Enforce return statements in getters 114 | "getter-return": 2, 115 | 116 | // Disallow await inside of loops 117 | "no-await-in-loop": 2, 118 | 119 | // Disallow comparing against -0 120 | "no-compare-neg-zero": 2, 121 | 122 | // Warn against catch clause parameters from shadowing variables in the outer scope 123 | "no-catch-shadow": 1, 124 | 125 | // Disallow identifiers from shadowing restricted names 126 | "no-shadow-restricted-names": 2, 127 | 128 | // Require error handling in callbacks 129 | "handle-callback-err": 2, 130 | 131 | // Warn against string concatenation with __dirname and __filename 132 | "no-path-concat": 1, 133 | 134 | // Prefer using arrow functions for callbacks 135 | "prefer-arrow-callback": 1, 136 | 137 | // Return inside each then() to create readable and reusable Promise chains. 138 | // Forces developers to return console logs and http calls in promises. 139 | "promise/always-return": 2, 140 | 141 | //Enforces the use of catch() on un-returned promises 142 | "promise/catch-or-return": 2, 143 | 144 | // Warn against nested then() or catch() statements 145 | "promise/no-nesting": 1 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/validators.test.js: -------------------------------------------------------------------------------- 1 | const validators = require('../lib/validators') 2 | const utils = require('./utils') 3 | 4 | const expectEachToBe = (array, cb, value) => 5 | array.forEach(item => { 6 | expect(cb(item)).toBe(value) 7 | }) 8 | 9 | describe('validators.js', () => { 10 | describe('isAfter', () => { 11 | 12 | }) 13 | 14 | describe('isAny', () => { 15 | const { isAny } = validators 16 | 17 | it('should return `true` for acceptable types', () => { 18 | expectEachToBe( 19 | utils.acceptable, 20 | isAny, 21 | true, 22 | ) 23 | }) 24 | 25 | it('should return `false` for non-acceptable types', () => { 26 | expectEachToBe( 27 | utils.nonAcceptable, 28 | isAny, 29 | false, 30 | ) 31 | }) 32 | }) 33 | 34 | describe('isArray', () => { 35 | const { isArray } = validators 36 | 37 | it('should return `true` for arrays', () => { 38 | expectEachToBe( 39 | utils.arrays, 40 | isArray, 41 | true, 42 | ) 43 | }) 44 | 45 | it('should return `false` for non-arrays', () => { 46 | expectEachToBe( 47 | utils.allExcept(utils.arrays), 48 | isArray, 49 | false, 50 | ) 51 | }) 52 | }) 53 | 54 | describe('isBefore', () => { 55 | 56 | }) 57 | 58 | describe('isBoolean', () => { 59 | const { isBoolean } = validators 60 | 61 | it('should return `true` for litteral booleans', () => { 62 | expectEachToBe( 63 | utils.booleans, 64 | isBoolean, 65 | true, 66 | ) 67 | }) 68 | 69 | it('should return `false` for non-booleans', () => { 70 | expectEachToBe( 71 | utils.allExcept(utils.booleans), 72 | isBoolean, 73 | false, 74 | ) 75 | }) 76 | }) 77 | 78 | describe('isDocumentReference', () => { 79 | const { isDocumentReference } = validators 80 | 81 | it('should return `true` for DocumentReferences', () => { 82 | expectEachToBe( 83 | utils.documentReferences, 84 | isDocumentReference, 85 | true, 86 | ) 87 | }) 88 | 89 | it('should return `false` for non-DocumentReferences', () => { 90 | expectEachToBe( 91 | utils.allExcept(utils.documentReferences), 92 | isDocumentReference, 93 | false, 94 | ) 95 | }) 96 | }) 97 | 98 | describe('isGeoPoint', () => { 99 | const { isGeoPoint } = validators 100 | 101 | it('should return `true` for GeoPoints', () => { 102 | expectEachToBe( 103 | utils.geoPoints, 104 | isGeoPoint, 105 | true, 106 | ) 107 | }) 108 | 109 | it('should return `false` for non-GeoPoints', () => { 110 | expectEachToBe( 111 | utils.allExcept(utils.geoPoints), 112 | isGeoPoint, 113 | false, 114 | ) 115 | }) 116 | }) 117 | 118 | describe('isInRange', () => { 119 | 120 | }) 121 | 122 | describe('isInteger', () => { 123 | const { isInteger } = validators 124 | 125 | it('should return `true` for integers', () => { 126 | expectEachToBe( 127 | utils.integers, 128 | isInteger, 129 | true, 130 | ) 131 | }) 132 | 133 | it('should return `false` for non-integers', () => { 134 | expectEachToBe( 135 | utils.allExcept(utils.integers), 136 | isInteger, 137 | false, 138 | ) 139 | }) 140 | }) 141 | 142 | describe('isMap', () => { 143 | const { isMap } = validators 144 | 145 | it('should return `true` for objects', () => { 146 | expectEachToBe( 147 | utils.maps, 148 | isMap, 149 | true, 150 | ) 151 | }) 152 | 153 | it('should return `false` for non-objects', () => { 154 | expectEachToBe( 155 | utils.allExcept(utils.maps), 156 | isMap, 157 | false, 158 | ) 159 | }) 160 | }) 161 | 162 | describe('isMatching', () => { 163 | 164 | }) 165 | 166 | describe('isNumber', () => { 167 | const { isNumber } = validators 168 | 169 | it('should return `true` for numbers', () => { 170 | expectEachToBe( 171 | [ 172 | ...utils.numbers, 173 | ...utils.integers, 174 | ], 175 | isNumber, 176 | true, 177 | ) 178 | }) 179 | 180 | it('should return `false` for non-numbers', () => { 181 | expectEachToBe( 182 | utils.allExcept([ 183 | ...utils.numbers, 184 | ...utils.integers, 185 | ]), 186 | isNumber, 187 | false, 188 | ) 189 | }) 190 | }) 191 | 192 | describe('isString', () => { 193 | const { isString } = validators 194 | 195 | it('should return `true` for strings', () => { 196 | expectEachToBe( 197 | utils.strings, 198 | isString, 199 | true, 200 | ) 201 | }) 202 | 203 | it('should return `false` for non-strings', () => { 204 | expectEachToBe( 205 | utils.allExcept(utils.strings), 206 | isString, 207 | false, 208 | ) 209 | }) 210 | }) 211 | 212 | describe('isTimestamp', () => { 213 | const { isTimestamp } = validators 214 | 215 | it('should return `true` for Timestamps', () => { 216 | expectEachToBe( 217 | utils.timestamps, 218 | isTimestamp, 219 | true, 220 | ) 221 | }) 222 | 223 | it('should return `false` for non-Timestamps', () => { 224 | expectEachToBe( 225 | utils.allExcept(utils.timestamps), 226 | isTimestamp, 227 | false, 228 | ) 229 | }) 230 | }) 231 | }) 232 | -------------------------------------------------------------------------------- /lib/Field.js: -------------------------------------------------------------------------------- 1 | const filters = require('./filters') 2 | const { asyncForEach } = require('./helpers') 3 | const ValidationError = require('./ValidationError') 4 | const StructureError = require('./StructureError') 5 | const Schema = require('./Schema') 6 | 7 | const OptionalSymbol = Symbol() 8 | 9 | /** 10 | * Definition of Field. 11 | * 12 | * @class Field 13 | */ 14 | class Field { 15 | /** 16 | * Creates an instance of Field. 17 | * 18 | * @param {String} _label Field's Label. 19 | * @memberof Field 20 | */ 21 | constructor (_label) { 22 | if (!_label || typeof _label !== 'string') 23 | throw new StructureError('Field Label must be defined.') 24 | 25 | this._label = _label 26 | this._hasChanged = false 27 | 28 | this._stack = [] 29 | 30 | this._objectOf 31 | this._arrayOf 32 | this._defaultValue 33 | 34 | this._isTypeDefined = false 35 | 36 | this._isNullable = false 37 | 38 | this._isOptional = false 39 | this._OptionalSymbol = OptionalSymbol 40 | } 41 | 42 | /** 43 | * Validates Field Data against Field. 44 | * 45 | * @param {*} fieldData Field Data. 46 | * @returns Validated Field Data. 47 | * @memberof Field 48 | */ 49 | async validate(fieldData) { 50 | if (this._defaultValue !== undefined && fieldData === undefined) 51 | fieldData = typeof this._defaultValue === 'function' 52 | ? await this._defaultValue() 53 | : this._defaultValue 54 | 55 | if (this._isOptional && fieldData === undefined) 56 | return this._OptionalSymbol 57 | 58 | if (this._isNullable && (fieldData === null || fieldData === undefined)) 59 | return null 60 | 61 | if (fieldData === undefined || fieldData === null) 62 | throw new ValidationError(`${this._label} is required.`) 63 | 64 | fieldData = await this.validateField(fieldData) 65 | 66 | if (this._objectOf) 67 | fieldData = this._objectOf instanceof Schema 68 | ? await this._objectOf.validate(fieldData) 69 | : await this.validateObject(fieldData) 70 | 71 | if (this._arrayOf) 72 | fieldData = await this.validateArray(fieldData) 73 | 74 | return fieldData 75 | } 76 | 77 | /** 78 | * Validated Field Data at high level. 79 | * 80 | * @param {*} fieldData Field Data. 81 | * @returns Validated Field Data. 82 | * @memberof Field 83 | */ 84 | async validateField(fieldData) { 85 | for (const filter of this._stack) { 86 | /* eslint no-await-in-loop: 0 */ 87 | try { 88 | fieldData = await filter(fieldData) 89 | } catch (error) { 90 | error.message = error.message.replace('%s', this._label) 91 | 92 | throw error 93 | } 94 | } 95 | 96 | return fieldData 97 | } 98 | 99 | /** 100 | * Validates nested Fields of Object Field. 101 | * 102 | * @param {*} fieldData Field Data. 103 | * @returns Validated Field Data. 104 | * @memberof Field 105 | */ 106 | async validateObject(fieldData) { 107 | const filteredData = {} 108 | 109 | await asyncForEach( 110 | Object.entries(this._objectOf), 111 | async ([key, field]) => { 112 | const filteredValue = await field.validate(fieldData[key]) 113 | 114 | if (filteredValue !== field._OptionalSymbol) 115 | filteredData[key] = filteredValue 116 | } 117 | ) 118 | 119 | return filteredData 120 | } 121 | 122 | /** 123 | * Validates nested Fields of Array Field. 124 | * 125 | * @param {*} arrayData Array Data. 126 | * @returns Validated Array Data. 127 | * @memberof Field 128 | */ 129 | async validateArray(arrayData) { 130 | const filteredData = [] 131 | 132 | await asyncForEach( 133 | arrayData, 134 | async (itemData) => { 135 | const filteredValue = await this._arrayOf.validate(itemData) 136 | 137 | if (filteredValue !== this._arrayOf._OptionalSymbol) 138 | filteredData.push(filteredValue) 139 | } 140 | ) 141 | 142 | return filteredData 143 | } 144 | 145 | /** 146 | * Sets _isTypeDefined to true, so Field can only be of one type. 147 | * 148 | * @memberof Field 149 | */ 150 | _defineType() { 151 | if (this._isTypeDefined) 152 | throw new StructureError('Type has already been defined.') 153 | 154 | this._isTypeDefined = true 155 | } 156 | 157 | /** 158 | * Adds filter to stack. 159 | * 160 | * @param {Function} filter 161 | * @returns {this} 162 | * @memberof Field 163 | */ 164 | _add(filter) { 165 | this._stack.push(filter) 166 | 167 | return this 168 | } 169 | 170 | /** 171 | * Adds custom filter to stack. 172 | * 173 | * @param {Function} filter 174 | * @returns {this} 175 | * @memberof Field 176 | */ 177 | custom(filter) { 178 | return this._add(filter) 179 | } 180 | 181 | /** 182 | * Defines default value that will be returned if Field Data is undefined. 183 | * 184 | * @param {*} defaultValue 185 | * @returns {this} 186 | * @memberof Field 187 | */ 188 | default(defaultValue) { 189 | this._defaultValue = defaultValue 190 | 191 | return this 192 | } 193 | 194 | /** 195 | * Allows Field Data to be null. 196 | * 197 | * @returns {this} 198 | * @memberof Field 199 | */ 200 | nullable() { 201 | this._isNullable = true 202 | 203 | return this 204 | } 205 | 206 | /** 207 | * Makes Field optional. 208 | * 209 | * @returns {this} 210 | * @memberof Field 211 | */ 212 | optional() { 213 | this._isOptional = true 214 | 215 | return this 216 | } 217 | 218 | /** 219 | * Defines Field as an Array with items defined by nested Field or Schema. 220 | * 221 | * @param {Field|Schema} fieldOrSchema Field or Schema. 222 | * @param {*} args 223 | * @returns {this} 224 | * @memberof Field 225 | */ 226 | arrayOf(fieldOrSchema, ...args) { 227 | this._defineType() 228 | this._arrayOf = fieldOrSchema 229 | 230 | return this._add(filters.array(...args)) 231 | } 232 | 233 | /** 234 | * Defines Field as an Object with entries defined by nested object of Fields or Schema. 235 | * 236 | * @param {Object|Schema} objectOfFieldsOrSchema Object of Fields or Schema. 237 | * @param {*} args 238 | * @returns {this} 239 | * @memberof Field 240 | */ 241 | objectOf(objectOfFieldsOrSchema, ...args) { 242 | this._defineType() 243 | this._objectOf = objectOfFieldsOrSchema 244 | 245 | return this._add(filters.object(...args)) 246 | } 247 | } 248 | 249 | const typeFilters = [ 250 | 'any', 251 | 'array', 252 | 'date', 253 | 'boolean', 254 | 'integer', 255 | 'number', 256 | 'object', 257 | 'oneOf', 258 | 'reference', 259 | 'string', 260 | 'timestamp', 261 | ] 262 | 263 | typeFilters.forEach(name => { 264 | if (!Field.prototype[name]) 265 | Field.prototype[name] = function (...args) { 266 | this._defineType() 267 | 268 | return this._add(filters[name](...args)) 269 | } 270 | }) 271 | 272 | Object.keys(filters) 273 | .forEach(name => { 274 | if (!Field.prototype[name]) 275 | Field.prototype[name] = function (...args) { 276 | return this._add(filters[name](...args)) 277 | } 278 | }) 279 | 280 | module.exports = Field 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firestore Schema Validator 2 | 3 | Elegant object modeling for Google Cloud Firestore. 4 | 5 | Inspired by [mongoose](https://github.com/Automattic/mongoose) and [datalize](https://github.com/flowstudio/datalize). 6 | 7 | ## Installation 8 | 9 | Requires `firebase-admin` package. 10 | 11 | ```bash 12 | npm install --save firestore-schema-validator 13 | ``` 14 | 15 | ## API Docs 16 | 17 | [DOCS.md](/DOCS.md) 18 | 19 | ## Usage 20 | 21 | ### Schema & Model - Simple example 22 | ```javascript 23 | // UserModel.js 24 | const { Model, schema, field } = require('firestore-schema-validator') 25 | 26 | const userSchema = schema({ 27 | firstName: field('First Name') 28 | .string() 29 | .trim(), 30 | lastName: field('Last Name') 31 | .string() 32 | .trim(), 33 | email: field('Email Address') 34 | .string() 35 | .email(), 36 | }) 37 | 38 | class UserModel extends Model { 39 | // Path to Cloud Firestore collection. 40 | static get _collectionPath() { 41 | return 'users' 42 | } 43 | 44 | // Model Schema. 45 | static get _schema() { 46 | return userSchema 47 | } 48 | } 49 | ``` 50 | 51 | ### Schema & Model - Robust example 52 | ```javascript 53 | // UserModel.js 54 | const { Model, schema, field } = require('firestore-schema-validator') 55 | 56 | const userSchema = schema({ 57 | firstName: field('First Name') 58 | .string() 59 | .trim(), 60 | lastName: field('Last Name') 61 | .string() 62 | .trim(), 63 | password: field('Password') 64 | .string() 65 | .match(/[A-Z]/, '%s must contain an uppercase letter.') 66 | .match(/[a-z]/, '%s must contain a lowercase letter.') 67 | .match(/[0-9]/, '%s must contain a digit.') 68 | .minLength(8), 69 | email: field('Email Address') 70 | .string() 71 | .email(), 72 | emailVerificationCode: field('Email Verification Code') 73 | .string() 74 | .nullable(), 75 | birthDate: field('Birth Date') 76 | .date('YYYY-MM-DD') 77 | .before( 78 | moment() 79 | .subtract(13, 'years') 80 | .toISOString(), 81 | 'You must be at least 13 years old.', 82 | ), 83 | options: field('Options') 84 | .objectOf({ 85 | lang: field('Language') 86 | .oneOf([ 87 | 'en-US', 88 | 'pl-PL' 89 | ]) 90 | .default('en-US'), 91 | }) 92 | }) 93 | 94 | class UserModel extends Model { 95 | static get _collectionPath() { 96 | return 'users' 97 | } 98 | 99 | static get _schema() { 100 | return userSchema 101 | } 102 | 103 | // You can define additional methods... 104 | static async getByEmail(email) { 105 | return await this.getBy('email', email) 106 | } 107 | 108 | // ... or getters. 109 | get isEmailVerified() { 110 | return Boolean(this._data.emailVerificationCode) 111 | } 112 | 113 | get fullName() { 114 | return `${this._data.firstName} ${this._data.lastName}` 115 | } 116 | 117 | // this.toJSON() by default returns this._data, 118 | // but you might want to display it differently 119 | // (eg. don't show password in responses, 120 | // combine firstName and lastName into fullName, etc.) 121 | toJSON() { 122 | return { 123 | id: this._id, // ID of Document stored in Cloud Firestore 124 | createdAt: this._createdAt, // ISO String format date of Document's creation. 125 | updatedAt: this._updatedAt, // ISO String format date of Document's last update. 126 | fullName: this.fullName, 127 | email: this.email, 128 | isEmailVerified: this.isEmailVerified, 129 | } 130 | } 131 | } 132 | 133 | // Fired when new user is successfully created and stored. 134 | UserModel.on('created', async (user) => { 135 | // eg. send Welcome Email to User 136 | }) 137 | 138 | // Fired when user is successfully updated and stored. 139 | UserModel.on('updated', async (user) => { 140 | // eg. log info to console 141 | }) 142 | 143 | // Fired when user is succsessfully deleted. 144 | UserModel.on('deleted', async (user) => { 145 | // eg. delete photos uploaded by User 146 | }) 147 | 148 | // Fired during user.validate() if user.email has changed, 149 | // but *before* validating the data. 150 | UserModel.prehook('email', (data, user) => { 151 | // eg. set emailVerificationCode 152 | }) 153 | 154 | // Fired during user.validate() if user.email has changed, 155 | // but *after* validating the data. 156 | UserModel.posthook('email', (data, user) => { 157 | // eg. send Email Verification Email to User 158 | }) 159 | 160 | UserModel.posthook('password', (data, user) => { 161 | // eg. hash password to store it securely 162 | }) 163 | ``` 164 | 165 | ### Working with UserModel 166 | 167 | ```javascript 168 | const admin = require('firebase-admin') 169 | const User = require('../UserModel.js') 170 | 171 | // Initialize Firebase 172 | admin.initailizeApp({ 173 | // config 174 | }) 175 | 176 | const user = await User.create({ 177 | firstName: 'Jon', 178 | lastName: 'Doe', 179 | email: 'jon.doe@example.com', 180 | password: 'J0nD03!@#', 181 | birthDate: '1990-01-10', 182 | }) // => instance of UserModel 183 | 184 | console.log(user.toJSON()) // => 185 | // { 186 | // id: 'x22sSpmaJek0CYS9KTsI'. 187 | // createdAt: '2019-06-14T15:46:55.108Z', 188 | // updatedAt: null, 189 | // fullName: 'Jon Doe', 190 | // email: 'jon.doe@example.com', 191 | // isEmailVerified: false, 192 | // } 193 | 194 | user.firstName = 'J' 195 | user.foo = 'bar' // Won't be stored as it's not defined in UserModel._schema 196 | await user.save() // => instance of UserModel 197 | 198 | console.log(user.toJSON()) // => 199 | // { 200 | // id: 'x22sSpmaJek0CYS9KTsI'. 201 | // createdAt: '2019-06-14T15:46:55.108Z', 202 | // updatedAt: null, 203 | // fullName: 'J Doe', 204 | // email: 'jon.doe@example.com', 205 | // isEmailVerified: false, 206 | // } 207 | 208 | const fetchedUser = await UserModel.getByEmail('jon.doe@example.com') // => instance of UserModel 209 | 210 | console.log(fetchedUser.toJSON()) // => 211 | // { 212 | // id: 'x22sSpmaJek0CYS9KTsI'. 213 | // createdAt: '2019-06-14T15:46:55.108Z', 214 | // updatedAt: null, 215 | // fullName: 'J Doe', 216 | // email: 'jon.doe@example.com', 217 | // isEmailVerified: false, 218 | // } 219 | 220 | await fetchedUser.delete() 221 | 222 | const nonExistingUser = await UserModel.getByEmail('jon.doe@example.com') // => null 223 | ``` 224 | 225 | ### Nesting Schema 226 | `field.arrayOf(fieldOrSchema)` and `field.objectOf(objectOfFieldsOrSchema)` accept instances of Schema as an argument, so you can reuse repeatable schemas: 227 | 228 | ```javascript 229 | const { schema, field } = require('firestore-schema-validator') 230 | 231 | const simplifiedAddressSchema = schema({ 232 | street: field('Street') 233 | .string() 234 | .trim(), 235 | countryCode: field('Country') 236 | .oneOf([ 237 | 'US', 238 | 'CA', 239 | ]), 240 | zipCode: field('ZIP Code') 241 | .string() 242 | .trim(), 243 | }) 244 | 245 | const userSchema = schema({ 246 | firstName: field('First Name') 247 | .string() 248 | .trim(), 249 | lastName: field('Last Name') 250 | .string() 251 | .trim(), 252 | mailingAddress: field('Mailing Address') 253 | .objectOf(simplifiedAddressSchema), 254 | }) 255 | 256 | const companySchema = schema({ 257 | name: field('Company Name') 258 | .string() 259 | .trim(), 260 | locations: field('Locations') 261 | .arrayOf(simplifiedAddressSchema), 262 | }) 263 | ``` 264 | 265 | ## TODO 266 | 267 | - `Field.prototype.unique()` that checks if the value provided to field is unique in the collection. 268 | -------------------------------------------------------------------------------- /lib/filters.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | const validators = require('./validators') 3 | const ValidationError = require('./ValidationError') 4 | const StructureError = require('./StructureError') 5 | 6 | const after = ( 7 | date, 8 | errorMessage = `%s must be after ${date}.`, 9 | ) => value => { 10 | if (!validators.isAfter(value)) 11 | throw new ValidationError(errorMessage) 12 | 13 | return value 14 | } 15 | 16 | const any = ( 17 | errorMessage = `%s must be of one of the types accepted by Cloud Firestore: ${validators.FIRESTORE_TYPES.join(', ')}.`, 18 | ) => value => { 19 | if (!validators.isAny(value)) 20 | throw new ValidationError(errorMessage) 21 | 22 | return value 23 | } 24 | 25 | const array = ( 26 | errorMessage = '%s must be an Array.', 27 | ) => value => { 28 | if (!validators.isArray(value)) 29 | throw new ValidationError(errorMessage) 30 | 31 | return value 32 | } 33 | 34 | const before = ( 35 | date, 36 | errorMessage = `%s must be before ${date}.`, 37 | ) => value => { 38 | if (!validators.isBefore(date, value)) 39 | throw new ValidationError(errorMessage) 40 | 41 | return value 42 | } 43 | 44 | const boolean = ( 45 | errorMessage = '%s must be a Boolean.', 46 | ) => value => { 47 | if (!validators.isBoolean(value)) 48 | throw new ValidationError(errorMessage) 49 | 50 | return value 51 | } 52 | 53 | const date = ( 54 | format, 55 | errorMessage = format 56 | ? `%s must be a valid Date in ${format} format.` 57 | : '%s must be a valid Date.', 58 | ) => value => { 59 | const date = moment(value, format) 60 | 61 | if (!date.isValid()) 62 | throw new ValidationError(errorMessage) 63 | 64 | if (format && date.format(format) !== value) 65 | throw new ValidationError(errorMessage) 66 | 67 | return value 68 | } 69 | 70 | const email = ( 71 | errorMessage = '%s must be a valid email.', 72 | ) => value => { 73 | const regex = /^[-\w\d.+_]+@[-\w\d.+_]+\.[\w]{2,}$/ 74 | 75 | if (!validators.isMatching(regex, value)) 76 | throw new ValidationError(errorMessage) 77 | 78 | return value.toLowerCase() 79 | } 80 | 81 | const oneOf = ( 82 | acceptableValues, 83 | errorMessage = `%s must be one of the accepted values (${acceptableValues.join(', ')}).`, 84 | ) => value => { 85 | if (!Array.isArray(acceptableValues) || acceptableValues.length === 0) 86 | throw new StructureError('Field.oneOf(): acceptableValues must be an array with at least one item.') 87 | 88 | if (!acceptableValues.every(value => 89 | validators.isBoolean(value) 90 | || validators.isNumber(value) 91 | || validators.isString(value) 92 | )) 93 | throw new StructureError('Field.oneOf(): each of acceptableValues must be of one of the accepted types (Boolean, Number, String).') 94 | 95 | if (!acceptableValues.includes(value)) 96 | throw new ValidationError(errorMessage) 97 | 98 | return value 99 | } 100 | 101 | const equal = ( 102 | compare, 103 | errorMessage = `%s must equal ${compare}.`, 104 | ) => value => { 105 | if (compare !== value) 106 | throw new ValidationError(errorMessage) 107 | 108 | return value 109 | } 110 | 111 | const geopoint = ( 112 | errorMessage = '% must be an instance of GeoPoint.', 113 | ) => value => { 114 | if (!validators.isGeoPoint(value)) 115 | throw new ValidationError(errorMessage) 116 | 117 | return value 118 | } 119 | 120 | const integer = ( 121 | errorMessage = '%s must be an Integer Number.', 122 | ) => value => { 123 | if (!validators.isInteger(value)) 124 | throw new ValidationError(errorMessage) 125 | 126 | return value 127 | } 128 | 129 | const length = ( 130 | length, 131 | errorMessage = `%s must have length of ${length}.`, 132 | ) => value => { 133 | if (value.length !== length) 134 | throw new ValidationError(errorMessage) 135 | 136 | return value 137 | } 138 | 139 | const match = ( 140 | regex, 141 | errorMessage = `%s must match ${regex} pattern.`, 142 | ) => value => { 143 | if (!validators.isMatching(regex, value)) 144 | throw new ValidationError(errorMessage) 145 | 146 | return value 147 | } 148 | 149 | const max = ( 150 | max, 151 | errorMessage = `%s must be a Number less than or equal to ${max}.`, 152 | ) => value => { 153 | if (!validators.isInRange(-Infinity, max, value)) 154 | throw new ValidationError(errorMessage) 155 | 156 | return value 157 | } 158 | 159 | const maxLength = ( 160 | maxLength, 161 | errorMessage = `%s must have length of at most ${maxLength}.`, 162 | ) => value => { 163 | if (!(value.length <= maxLength)) 164 | throw new ValidationError(errorMessage) 165 | 166 | return value 167 | } 168 | 169 | const min = ( 170 | min, 171 | errorMessage = `%s must be a Number greater than or equal to ${min}.`, 172 | ) => value => { 173 | if (!validators.isInRange(min, Infinity, value)) 174 | throw new ValidationError(errorMessage) 175 | 176 | return value 177 | } 178 | 179 | const minLength = ( 180 | minLength, 181 | errorMessage = `%s must have length of at least ${minLength}.`, 182 | ) => value => { 183 | if (!(minLength <= value.length)) 184 | throw new ValidationError(errorMessage) 185 | 186 | return value 187 | } 188 | 189 | const number = ( 190 | errorMessage = '%s must be a Number.', 191 | ) => value => { 192 | if (!validators.isNumber(value)) 193 | throw new ValidationError(errorMessage) 194 | 195 | return value 196 | } 197 | 198 | const object = ( 199 | errorMessage = '%s must be a Map.', 200 | ) => value => { 201 | if (!validators.isMap(value)) 202 | throw new ValidationError(errorMessage) 203 | 204 | return value 205 | } 206 | 207 | const range = ( 208 | min, 209 | max, 210 | errorMessage = `%s must be a Number between ${min} and ${max}`, 211 | ) => value => { 212 | if (!validators.isInRange(min, max, value)) 213 | throw new ValidationError(errorMessage) 214 | 215 | return value 216 | } 217 | 218 | const reference = ( 219 | errorMessage = '%s must be an instance of DocumentReference.', 220 | ) => value => { 221 | if (!validators.isDocumentReference(value)) 222 | throw new ValidationError(errorMessage) 223 | 224 | return value 225 | } 226 | 227 | const string = ( 228 | errorMessage = '%s must be a String.', 229 | ) => value => { 230 | if (!validators.isString(value)) 231 | throw new ValidationError(errorMessage) 232 | 233 | return value 234 | } 235 | 236 | const timestamp = ( 237 | errorMessage = '%s must be an instance of Timestamp.', 238 | ) => value => { 239 | if (!validators.isTimestamp(value)) 240 | throw new ValidationError(errorMessage) 241 | 242 | return value 243 | } 244 | 245 | const trim = ( 246 | errorMessage = 'Couldn\'t trim %s.', 247 | ) => value => { 248 | try { 249 | return value.trim() 250 | } catch (err) { 251 | throw new ValidationError(errorMessage) 252 | } 253 | } 254 | 255 | const toLowerCase = ( 256 | errorMessage = 'Couldn\'t turn %s to lower case.', 257 | ) => value => { 258 | try { 259 | return value.toLowerCase() 260 | } catch (err) { 261 | throw new ValidationError(errorMessage) 262 | } 263 | } 264 | 265 | const toUpperCase = ( 266 | errorMessage = 'Couldn\'t turn %s to upper case.', 267 | ) => value => { 268 | try { 269 | return value.toUpperCase() 270 | } catch (err) { 271 | throw new ValidationError(errorMessage) 272 | } 273 | } 274 | 275 | module.exports = { 276 | after, 277 | any, 278 | array, 279 | before, 280 | boolean, 281 | date, 282 | email, 283 | equal, 284 | geopoint, 285 | integer, 286 | length, 287 | number, 288 | match, 289 | max, 290 | maxLength, 291 | min, 292 | minLength, 293 | object, 294 | oneOf, 295 | range, 296 | reference, 297 | string, 298 | timestamp, 299 | toLowerCase, 300 | toUpperCase, 301 | trim, 302 | } 303 | -------------------------------------------------------------------------------- /lib/Model.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | const moment = require('moment') 3 | const EventEmitter = require('events') 4 | 5 | const ModelProxy = require('./ModelProxy') 6 | const Schema = require('./Schema') 7 | 8 | const { asyncForEach, markAsChanged } = require('./helpers') 9 | 10 | /** 11 | * Boilerplate ODM to interact with Cloud Firestore. 12 | * Must be extended. 13 | * 14 | * @class Model 15 | */ 16 | class Model { 17 | /** 18 | * Creates an instance of Model. 19 | * 20 | * @param {DocumentSnapshot} _snapshot - Document Snapshot. 21 | * @param {Object} _data - Document Data. 22 | * @returns {Proxy} ModelProxy which handles data setters and getters. 23 | * @memberof Model 24 | */ 25 | constructor (_snapshot, _data) { 26 | if (this.constructor === Model) 27 | throw new Error('Model can\'t be used directly and must be extended instead.') 28 | 29 | const name = this.constructor.name 30 | 31 | if (!(_snapshot instanceof admin.firestore.DocumentSnapshot)) 32 | throw new Error(`${name} constructor must be called with instance of DocumentSnapshot.`) 33 | 34 | if (!this.constructor._collectionPath) 35 | throw new Error(`${name} must have a static getter _collectionPath.`) 36 | 37 | if (typeof this.constructor._collectionPath !== 'string') 38 | throw new Error(`${name}'s static getter _collectionPath must return a string.`) 39 | 40 | if (!this.constructor._schema) 41 | throw new Error(`${name} must have a static getter _schema.`) 42 | 43 | if (!(this.constructor._schema instanceof Schema)) 44 | throw new Error(`${name}'s static getter _schema must return an instance of Schema.`) 45 | 46 | this._snapshot = _snapshot 47 | this._data = _data || this._snapshot.data() || {} 48 | this._changedKeys = new Set() 49 | 50 | this._proxy = new ModelProxy(this) 51 | 52 | return this._proxy 53 | } 54 | 55 | 56 | /** 57 | * ID of Document. 58 | * 59 | * @readonly 60 | * @type {String} 61 | * @memberof Model 62 | */ 63 | get _id() { 64 | return this._snapshot.id 65 | } 66 | 67 | /** 68 | * Date of Document creation in ISO String format. 69 | * 70 | * @readonly 71 | * @type {String} 72 | * @memberof Model 73 | */ 74 | get _createdAt() { 75 | const createTime = this._snapshot.createTime 76 | 77 | if (!createTime) 78 | return moment() 79 | .toISOString() 80 | 81 | return moment 82 | .unix(createTime.seconds) 83 | .toISOString() 84 | } 85 | 86 | /** 87 | * Date of Document update in ISO String format. 88 | * 89 | * @readonly 90 | * @type {String} 91 | * @memberof Model 92 | */ 93 | get _updatedAt() { 94 | const updateTime = this._snapshot.updateTime 95 | 96 | if (!updateTime) 97 | return null 98 | 99 | return moment 100 | .unix(updateTime.seconds) 101 | .toISOString() 102 | } 103 | 104 | /** 105 | * Collection Path. 106 | * 107 | * @readonly 108 | * @type {String} 109 | * @memberof Model 110 | */ 111 | get _collectionPath() { 112 | return this.constructor._collectionPath 113 | } 114 | 115 | /** 116 | * Collection Reference. 117 | * 118 | * @readonly 119 | * @static 120 | * @type {CollectionReference} 121 | * @memberof Model 122 | */ 123 | static get _collectionRef() { 124 | return admin 125 | .firestore() 126 | .collection(this._collectionPath) 127 | } 128 | 129 | /** 130 | * Collection Reference. 131 | * 132 | * @readonly 133 | * @type {String} 134 | * @memberof Model 135 | */ 136 | get _collectionRef() { 137 | return this.constructor._collectionRef 138 | } 139 | 140 | /** 141 | * Document Reference. 142 | * 143 | * @readonly 144 | * @type {String} 145 | * @memberof Model 146 | */ 147 | get _docRef() { 148 | return this._collectionRef 149 | .doc(this._id) 150 | } 151 | 152 | /** 153 | * Instance of EventEmitter used with this.on() and this.emit(). 154 | * 155 | * @readonly 156 | * @static 157 | * @type {EventEmitter} 158 | * @memberof Model 159 | */ 160 | static get _events() { 161 | if (!this._emitter) 162 | this._emitter = new EventEmitter() 163 | 164 | return this._emitter 165 | } 166 | 167 | /** 168 | * Subsribes to event. 169 | * 170 | * @static 171 | * @param {String} event - Event name. 172 | * @param {Function} cb - Callback function. 173 | * @memberof Model 174 | */ 175 | static on(event, cb) { 176 | this._events.on(event, cb) 177 | } 178 | 179 | /** 180 | * Emits event. 181 | * 182 | * @param {String} event - Event name. 183 | * @memberof Model 184 | */ 185 | emit(event) { 186 | this.constructor._events.emit(event, this) 187 | } 188 | 189 | /** 190 | * Adds hook that will be fired before parsing data 191 | * if this[path] has changed. 192 | * 193 | * @static 194 | * @param {String} path - Path of property. 195 | * @param {Function} cb - Callback function. 196 | * @memberof Model 197 | */ 198 | static prehook(path, cb) { 199 | if (!this._prehooks) 200 | this._prehooks = {} 201 | 202 | if (!Array.isArray(this._prehooks[path])) 203 | this._prehooks[path] = [] 204 | 205 | this._prehooks[path].push(cb) 206 | } 207 | 208 | /** 209 | * Adds hook that will be fired after parsing data 210 | * if this[path] has changed. 211 | * 212 | * @static 213 | * @param {String} path - Path of property. 214 | * @param {Function} cb - Callback function. 215 | * @memberof Model 216 | */ 217 | static posthook(path, cb) { 218 | if (!this._posthooks) 219 | this._posthooks = {} 220 | 221 | if (!Array.isArray(this._posthooks[path])) 222 | this._posthooks[path] = [] 223 | 224 | this._posthooks[path].push(cb) 225 | } 226 | 227 | /** 228 | * Fetches Document by ID. 229 | * 230 | * @static 231 | * @param {String} id 232 | * @returns {this|null} Instance of Model or null. 233 | * @memberof Model 234 | */ 235 | static async getById(id) { 236 | const snapshot = await this._collectionRef 237 | .doc(id) 238 | .get() 239 | 240 | if (!snapshot.exists) 241 | return null 242 | 243 | return new this(snapshot) 244 | } 245 | 246 | /** 247 | * Fetches Document by key and value pair. 248 | * 249 | * @static 250 | * @param {String} key - Key. 251 | * @param {*} value - Value to compare. 252 | * @returns {this|null} Instance of this or null. 253 | * @memberof Model 254 | */ 255 | static async getBy(key, value) { 256 | const querySnapshot = await this._collectionRef 257 | .where(key, '==', value) 258 | .limit(1) 259 | .get() 260 | 261 | if (!querySnapshot.docs.length) 262 | return null 263 | 264 | const snapshot = querySnapshot.docs[0] 265 | 266 | return new this(snapshot) 267 | } 268 | 269 | /** 270 | * Fetches all Documents by key and value pair. 271 | * 272 | * @static 273 | * @param {String} key 274 | * @param {*} value 275 | * @param {array} optionalModifiers 276 | * @returns {Array} Array of instances of this. 277 | * @memberof Model 278 | */ 279 | static async getAllBy(key, value, optionalModifiers) { 280 | let query = this._collectionRef 281 | .where(key, '==', value) 282 | 283 | if (Array.isArray(optionalModifiers) && optionalModifiers.length > 0) 284 | for (const modifier of optionalModifiers) 285 | query = query[modifier.key](...modifier.args) 286 | 287 | const querySnapshot = await query.get() 288 | 289 | return querySnapshot.docs 290 | .map(snapshot => new this(snapshot)) 291 | } 292 | 293 | /** 294 | * Creates new Document. 295 | * 296 | * @static 297 | * @param {Object} [data={}] 298 | * @returns Instance of this. 299 | * @memberof Model 300 | */ 301 | static async create(data = {}) { 302 | const snapshot = await this._collectionRef.doc() 303 | .get() 304 | 305 | const instance = new this(snapshot, data) 306 | markAsChanged(instance._changedKeys, data) 307 | 308 | instance._data = await instance.parseData(data, true) 309 | 310 | await instance._docRef 311 | .set(instance._data) 312 | 313 | instance.emit('created') 314 | 315 | return instance 316 | } 317 | 318 | /** 319 | * Deletes Document. 320 | * 321 | * @memberof Model 322 | */ 323 | async delete() { 324 | await this._docRef 325 | .delete() 326 | 327 | this.emit('deleted') 328 | } 329 | 330 | /** 331 | * Saves changes made to Document. 332 | * 333 | * @param {*} options 334 | * @returns This. 335 | * @memberof Model 336 | */ 337 | async save(options) { 338 | const data = await this.parseData() 339 | 340 | await this._docRef 341 | .set(data, options) 342 | 343 | this._data = data 344 | 345 | this.emit('updated') 346 | 347 | return this 348 | } 349 | 350 | /** 351 | * Validates Document Data. 352 | * 353 | * @param {*} [data={}] 354 | * @param {boolean} [all=false] 355 | * @returns Validated Data. 356 | * @memberof Model 357 | */ 358 | async validate(data = {}, all = false) { 359 | if (all) 360 | return await this.constructor._schema.validate(data) 361 | 362 | return await this.constructor._schema.validateSelected(data, this._changedKeys) 363 | } 364 | 365 | /** 366 | * Runs hooks on Document Data. 367 | * 368 | * @param {Object} hooks 369 | * @param {Object} [data={}] 370 | * @returns Updated Document Data. 371 | * @memberof Model 372 | */ 373 | async runHooks(hooks, data = {}) { 374 | /* eslint no-await-in-loop: 0 */ 375 | /* eslint no-loop-func: 0 */ 376 | if (!hooks) 377 | return data 378 | 379 | for (const changedKey of this._changedKeys.keys()) 380 | if (Array.isArray(hooks[changedKey])) 381 | await asyncForEach( 382 | hooks[changedKey], 383 | async (cb) => await cb(data, this), 384 | ) 385 | 386 | return data 387 | } 388 | 389 | /** 390 | * Parses Document Data, running hooks and validating it. 391 | * 392 | * @param {*} [data=this._data] 393 | * @param {boolean} [all=false] 394 | * @returns Updated and Validated Document Data. 395 | * @memberof Model 396 | */ 397 | async parseData(data = this._data, all = false) { 398 | data = await this.runHooks(this.constructor._prehooks, data) 399 | data = await this.validate(data, all) 400 | data = await this.runHooks(this.constructor._posthooks, data) 401 | 402 | this._changedKeys = new Set() 403 | 404 | return data 405 | } 406 | 407 | /** 408 | * Exposes public data to be shown in API responses. 409 | * 410 | * @returns {Object} 411 | * @memberof Model 412 | */ 413 | toJSON() { 414 | return this._data 415 | } 416 | } 417 | 418 | module.exports = Model 419 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 |
4 |
Field
5 |
6 |
Model
7 |
8 |
Schema
9 |
10 |
11 | 12 | ## Functions 13 | 14 |
15 |
asyncForEach(array, callback)
16 |

Runs async code for each item in array.

17 |
18 |
19 | 20 | 21 | 22 | ## Field 23 | **Kind**: global class 24 | 25 | * [Field](#Field) 26 | * [new Field()](#new_Field_new) 27 | * _instance_ 28 | * [.validate(fieldData)](#Field+validate) ⇒ 29 | * [.validateField(fieldData)](#Field+validateField) ⇒ 30 | * [.validateObject(fieldData)](#Field+validateObject) ⇒ 31 | * [.validateArray(arrayData)](#Field+validateArray) ⇒ 32 | * [._defineType()](#Field+_defineType) 33 | * [._add(filter)](#Field+_add) ⇒ this 34 | * [.custom(filter)](#Field+custom) ⇒ this 35 | * [.default(defaultValue)](#Field+default) ⇒ this 36 | * [.nullable()](#Field+nullable) ⇒ this 37 | * [.optional()](#Field+optional) ⇒ this 38 | * [.arrayOf(fieldOrSchema, ...args)](#Field+arrayOf) ⇒ this 39 | * [.objectOf(objectOfFieldsOrSchema, ...args)](#Field+objectOf) ⇒ this 40 | * _static_ 41 | * [.Field](#Field.Field) 42 | * [new Field(_label)](#new_Field.Field_new) 43 | 44 | 45 | 46 | ### new Field() 47 | Definition of Field. 48 | 49 | 50 | 51 | ### field.validate(fieldData) ⇒ 52 | Validates Field Data against Field. 53 | 54 | **Kind**: instance method of [Field](#Field) 55 | **Returns**: Validated Field Data. 56 | 57 | | Param | Type | Description | 58 | | --- | --- | --- | 59 | | fieldData | \* | Field Data. | 60 | 61 | 62 | 63 | ### field.validateField(fieldData) ⇒ 64 | Validated Field Data at high level. 65 | 66 | **Kind**: instance method of [Field](#Field) 67 | **Returns**: Validated Field Data. 68 | 69 | | Param | Type | Description | 70 | | --- | --- | --- | 71 | | fieldData | \* | Field Data. | 72 | 73 | 74 | 75 | ### field.validateObject(fieldData) ⇒ 76 | Validates nested Fields of Object Field. 77 | 78 | **Kind**: instance method of [Field](#Field) 79 | **Returns**: Validated Field Data. 80 | 81 | | Param | Type | Description | 82 | | --- | --- | --- | 83 | | fieldData | \* | Field Data. | 84 | 85 | 86 | 87 | ### field.validateArray(arrayData) ⇒ 88 | Validates nested Fields of Array Field. 89 | 90 | **Kind**: instance method of [Field](#Field) 91 | **Returns**: Validated Array Data. 92 | 93 | | Param | Type | Description | 94 | | --- | --- | --- | 95 | | arrayData | \* | Array Data. | 96 | 97 | 98 | 99 | ### field.\_defineType() 100 | Sets _isTypeDefined to true, so Field can only be of one type. 101 | 102 | **Kind**: instance method of [Field](#Field) 103 | 104 | 105 | ### field.\_add(filter) ⇒ this 106 | Adds filter to stack. 107 | 108 | **Kind**: instance method of [Field](#Field) 109 | 110 | | Param | Type | 111 | | --- | --- | 112 | | filter | function | 113 | 114 | 115 | 116 | ### field.custom(filter) ⇒ this 117 | Adds custom filter to stack. 118 | 119 | **Kind**: instance method of [Field](#Field) 120 | 121 | | Param | Type | 122 | | --- | --- | 123 | | filter | function | 124 | 125 | 126 | 127 | ### field.default(defaultValue) ⇒ this 128 | Defines default value that will be returned if Field Data is undefined. 129 | 130 | **Kind**: instance method of [Field](#Field) 131 | 132 | | Param | Type | 133 | | --- | --- | 134 | | defaultValue | \* | 135 | 136 | 137 | 138 | ### field.nullable() ⇒ this 139 | Allows Field Data to be null. 140 | 141 | **Kind**: instance method of [Field](#Field) 142 | 143 | 144 | ### field.optional() ⇒ this 145 | Makes Field optional. 146 | 147 | **Kind**: instance method of [Field](#Field) 148 | 149 | 150 | ### field.arrayOf(fieldOrSchema, ...args) ⇒ this 151 | Defines Field as an Array with items defined by nested Field or Schema. 152 | 153 | **Kind**: instance method of [Field](#Field) 154 | 155 | | Param | Type | Description | 156 | | --- | --- | --- | 157 | | fieldOrSchema | [Field](#Field) \| [Schema](#Schema) | Field or Schema. | 158 | | ...args | \* | | 159 | 160 | 161 | 162 | ### field.objectOf(objectOfFieldsOrSchema, ...args) ⇒ this 163 | Defines Field as an Object with entries defined by nested object of Fields or Schema. 164 | 165 | **Kind**: instance method of [Field](#Field) 166 | 167 | | Param | Type | Description | 168 | | --- | --- | --- | 169 | | objectOfFieldsOrSchema | [Object.<Field>](#Field) \| [Schema](#Schema) | Object of Fields or Schema. | 170 | | ...args | \* | | 171 | 172 | 173 | 174 | ### Field.Field 175 | **Kind**: static class of [Field](#Field) 176 | 177 | 178 | #### new Field(_label) 179 | Creates an instance of Field. 180 | 181 | 182 | | Param | Type | Description | 183 | | --- | --- | --- | 184 | | _label | String | Field's Label. | 185 | 186 | 187 | 188 | ## Model 189 | **Kind**: global class 190 | 191 | * [Model](#Model) 192 | * [new Model()](#new_Model_new) 193 | * _instance_ 194 | * [._id](#Model+_id) : String 195 | * [._createdAt](#Model+_createdAt) : String 196 | * [._updatedAt](#Model+_updatedAt) : String 197 | * [._collectionPath](#Model+_collectionPath) : String 198 | * [._collectionRef](#Model+_collectionRef) : String 199 | * [._docRef](#Model+_docRef) : String 200 | * [.emit(event)](#Model+emit) 201 | * [.delete()](#Model+delete) 202 | * [.save(options)](#Model+save) ⇒ 203 | * [.validate([data], [all])](#Model+validate) ⇒ 204 | * [.runHooks(hooks, [data])](#Model+runHooks) ⇒ 205 | * [.parseData([data], [all])](#Model+parseData) ⇒ 206 | * [.toJSON()](#Model+toJSON) ⇒ Object 207 | * _static_ 208 | * [.Model](#Model.Model) 209 | * [new Model(_snapshot, _data)](#new_Model.Model_new) 210 | * [._collectionRef](#Model._collectionRef) : CollectionReference 211 | * [._events](#Model._events) : EventEmitter 212 | * [.on(event, cb)](#Model.on) 213 | * [.prehook(path, cb)](#Model.prehook) 214 | * [.posthook(path, cb)](#Model.posthook) 215 | * [.getById(id)](#Model.getById) ⇒ this \| null 216 | * [.getBy(key, value)](#Model.getBy) ⇒ this \| null 217 | * [.getAllBy(key, value, optionalModifiers)](#Model.getAllBy) ⇒ Array.<this> 218 | * [.create([data])](#Model.create) ⇒ 219 | 220 | 221 | 222 | ### new Model() 223 | Boilerplate ODM to interact with Cloud Firestore. 224 | Must be extended. 225 | 226 | 227 | 228 | ### model.\_id : String 229 | ID of Document. 230 | 231 | **Kind**: instance property of [Model](#Model) 232 | **Read only**: true 233 | 234 | 235 | ### model.\_createdAt : String 236 | Date of Document creation in ISO String format. 237 | 238 | **Kind**: instance property of [Model](#Model) 239 | **Read only**: true 240 | 241 | 242 | ### model.\_updatedAt : String 243 | Date of Document update in ISO String format. 244 | 245 | **Kind**: instance property of [Model](#Model) 246 | **Read only**: true 247 | 248 | 249 | ### model.\_collectionPath : String 250 | Collection Path. 251 | 252 | **Kind**: instance property of [Model](#Model) 253 | **Read only**: true 254 | 255 | 256 | ### model.\_collectionRef : String 257 | Collection Reference. 258 | 259 | **Kind**: instance property of [Model](#Model) 260 | **Read only**: true 261 | 262 | 263 | ### model.\_docRef : String 264 | Document Reference. 265 | 266 | **Kind**: instance property of [Model](#Model) 267 | **Read only**: true 268 | 269 | 270 | ### model.emit(event) 271 | Emits event. 272 | 273 | **Kind**: instance method of [Model](#Model) 274 | 275 | | Param | Type | Description | 276 | | --- | --- | --- | 277 | | event | String | Event name. | 278 | 279 | 280 | 281 | ### model.delete() 282 | Deletes Document. 283 | 284 | **Kind**: instance method of [Model](#Model) 285 | 286 | 287 | ### model.save(options) ⇒ 288 | Saves changes made to Document. 289 | 290 | **Kind**: instance method of [Model](#Model) 291 | **Returns**: This. 292 | 293 | | Param | Type | 294 | | --- | --- | 295 | | options | \* | 296 | 297 | 298 | 299 | ### model.validate([data], [all]) ⇒ 300 | Validates Document Data. 301 | 302 | **Kind**: instance method of [Model](#Model) 303 | **Returns**: Validated Data. 304 | 305 | | Param | Type | Default | 306 | | --- | --- | --- | 307 | | [data] | \* | {} | 308 | | [all] | boolean | false | 309 | 310 | 311 | 312 | ### model.runHooks(hooks, [data]) ⇒ 313 | Runs hooks on Document Data. 314 | 315 | **Kind**: instance method of [Model](#Model) 316 | **Returns**: Updated Document Data. 317 | 318 | | Param | Type | Default | 319 | | --- | --- | --- | 320 | | hooks | Object | | 321 | | [data] | Object | {} | 322 | 323 | 324 | 325 | ### model.parseData([data], [all]) ⇒ 326 | Parses Document Data, running hooks and validating it. 327 | 328 | **Kind**: instance method of [Model](#Model) 329 | **Returns**: Updated and Validated Document Data. 330 | 331 | | Param | Type | Default | 332 | | --- | --- | --- | 333 | | [data] | \* | this._data | 334 | | [all] | boolean | false | 335 | 336 | 337 | 338 | ### model.toJSON() ⇒ Object 339 | Exposes public data to be shown in API responses. 340 | 341 | **Kind**: instance method of [Model](#Model) 342 | 343 | 344 | ### Model.Model 345 | **Kind**: static class of [Model](#Model) 346 | 347 | 348 | #### new Model(_snapshot, _data) 349 | Creates an instance of Model. 350 | 351 | **Returns**: Proxy - ModelProxy which handles data setters and getters. 352 | 353 | | Param | Type | Description | 354 | | --- | --- | --- | 355 | | _snapshot | DocumentSnapshot | Document Snapshot. | 356 | | _data | Object | Document Data. | 357 | 358 | 359 | 360 | ### Model.\_collectionRef : CollectionReference 361 | Collection Reference. 362 | 363 | **Kind**: static property of [Model](#Model) 364 | **Read only**: true 365 | 366 | 367 | ### Model.\_events : EventEmitter 368 | Instance of EventEmitter used with this.on() and this.emit(). 369 | 370 | **Kind**: static property of [Model](#Model) 371 | **Read only**: true 372 | 373 | 374 | ### Model.on(event, cb) 375 | Subsribes to event. 376 | 377 | **Kind**: static method of [Model](#Model) 378 | 379 | | Param | Type | Description | 380 | | --- | --- | --- | 381 | | event | String | Event name. | 382 | | cb | function | Callback function. | 383 | 384 | 385 | 386 | ### Model.prehook(path, cb) 387 | Adds hook that will be fired before parsing data 388 | if this[path] has changed. 389 | 390 | **Kind**: static method of [Model](#Model) 391 | 392 | | Param | Type | Description | 393 | | --- | --- | --- | 394 | | path | String | Path of property. | 395 | | cb | function | Callback function. | 396 | 397 | 398 | 399 | ### Model.posthook(path, cb) 400 | Adds hook that will be fired after parsing data 401 | if this[path] has changed. 402 | 403 | **Kind**: static method of [Model](#Model) 404 | 405 | | Param | Type | Description | 406 | | --- | --- | --- | 407 | | path | String | Path of property. | 408 | | cb | function | Callback function. | 409 | 410 | 411 | 412 | ### Model.getById(id) ⇒ this \| null 413 | Fetches Document by ID. 414 | 415 | **Kind**: static method of [Model](#Model) 416 | **Returns**: this \| null - Instance of Model or null. 417 | 418 | | Param | Type | 419 | | --- | --- | 420 | | id | String | 421 | 422 | 423 | 424 | ### Model.getBy(key, value) ⇒ this \| null 425 | Fetches Document by key and value pair. 426 | 427 | **Kind**: static method of [Model](#Model) 428 | **Returns**: this \| null - Instance of this or null. 429 | 430 | | Param | Type | Description | 431 | | --- | --- | --- | 432 | | key | String | Key. | 433 | | value | \* | Value to compare. | 434 | 435 | 436 | 437 | ### Model.getAllBy(key, value, optionalModifiers) ⇒ Array.<this> 438 | Fetches all Documents by key and value pair. 439 | 440 | **Kind**: static method of [Model](#Model) 441 | **Returns**: Array.<this> - Array of instances of this. 442 | 443 | | Param | Type | 444 | | --- | --- | 445 | | key | String | 446 | | value | \* | 447 | | optionalModifiers | array | 448 | 449 | 450 | 451 | ### Model.create([data]) ⇒ 452 | Creates new Document. 453 | 454 | **Kind**: static method of [Model](#Model) 455 | **Returns**: Instance of this. 456 | 457 | | Param | Type | Default | 458 | | --- | --- | --- | 459 | | [data] | Object | {} | 460 | 461 | 462 | 463 | ## Schema 464 | **Kind**: global class 465 | 466 | * [Schema](#Schema) 467 | * [new Schema()](#new_Schema_new) 468 | * _instance_ 469 | * [.validate([data], [fields])](#Schema+validate) ⇒ 470 | * [.validateSelected([data], [changedKeys])](#Schema+validateSelected) ⇒ 471 | * _static_ 472 | * [.Schema](#Schema.Schema) 473 | * [new Schema(_fields)](#new_Schema.Schema_new) 474 | 475 | 476 | 477 | ### new Schema() 478 | Definition of Document Schema. 479 | 480 | 481 | 482 | ### schema.validate([data], [fields]) ⇒ 483 | Validates Document Data agains Fields. 484 | 485 | **Kind**: instance method of [Schema](#Schema) 486 | **Returns**: Validated Document Data. 487 | 488 | | Param | Type | Default | 489 | | --- | --- | --- | 490 | | [data] | Object | {} | 491 | | [fields] | [Object.<Field>](#Field) | this._fields | 492 | 493 | 494 | 495 | ### schema.validateSelected([data], [changedKeys]) ⇒ 496 | Validates Document Data against selected Fields. 497 | 498 | **Kind**: instance method of [Schema](#Schema) 499 | **Returns**: Valdiated Document Data. 500 | 501 | | Param | Type | Default | Description | 502 | | --- | --- | --- | --- | 503 | | [data] | Object | {} | Document Data | 504 | | [changedKeys] | Set | new Set() | Set with Paths of changed Fields. | 505 | 506 | 507 | 508 | ### Schema.Schema 509 | **Kind**: static class of [Schema](#Schema) 510 | 511 | 512 | #### new Schema(_fields) 513 | Creates an instance of Schema. 514 | 515 | 516 | | Param | Type | Description | 517 | | --- | --- | --- | 518 | | _fields | [Object.<Field>](#Field) | Object containing Field definitions. | 519 | 520 | 521 | 522 | ## asyncForEach(array, callback) 523 | Runs async code for each item in array. 524 | 525 | **Kind**: global function 526 | 527 | | Param | Type | Description | 528 | | --- | --- | --- | 529 | | array | Array | Array of items. | 530 | | callback | function | Callback for each item. | 531 | 532 | --------------------------------------------------------------------------------