diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 7b37dd5..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a9cab43 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +/coverage +/logo +/typings +/lib +!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2fc3d83 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = { + root: true, + parserOptions: { + sourceType: 'script', + }, + extends: ['airbnb-base', 'plugin:prettier/recommended'], + env: { + node: true, + mocha: true, + }, + plugins: ['mocha'], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx'], + }, + }, + }, + rules: { + strict: ['error', 'global'], + 'arrow-parens': ['error', 'as-needed'], + indent: [ + 'error', + 2, + { + SwitchCase: 1, + }, + ], + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: ['**/test/**/*.js'], + }, + ], + 'no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + 'import/prefer-default-export': 'off', + 'prefer-rest-params': 'off', + 'prefer-spread': 'off', + 'no-restricted-globals': 'off', + 'no-underscore-dangle': 'off', + 'no-param-reassign': 'off', + 'max-len': ['error', { code: 120, ignoreUrls: true }], + 'mocha/no-exclusive-tests': 'error', + }, +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index be50769..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "script" - }, - "root": true, - "env": { - "node": true, - "mocha": true - }, - "extends": "airbnb-base", - "plugins": [ - "mocha" - ], - "rules": { - "arrow-parens": ["error", "as-needed"], - "indent": [ - "error", - 4, - { - "SwitchCase": 1 - } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "**/test/**/*.js" - ] - } - ], - "no-use-before-define": [ - "error", - { - "functions": false - } - ], - "prefer-rest-params": "off", - "prefer-spread": "off", - "no-restricted-globals": "off", - "no-underscore-dangle": "off", - "no-param-reassign": "off", - "max-len": [ - "error", - 120 - ], - "mocha/no-exclusive-tests": "error", - "comma-dangle": [ - "error", - { - "arrays": "always-multiline", - "objects": "always-multiline", - "functions": "ignore" - } - ], - "strict": ["error", "global"] - } -} diff --git a/.gitignore b/.gitignore index 6223f8f..67973d7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ coverage # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release +dist/ +lib/ # Dependency directory node_modules diff --git a/.npmignore b/.npmignore index 2f96932..1c891df 100644 --- a/.npmignore +++ b/.npmignore @@ -2,16 +2,20 @@ jsconfig.json .travis.yml .jshintrc -.eslintrc.json +.eslintignore +.eslintrc.js +.prettierrc.js .babelrc .editorconfig +tsconfig.json **/todo.txt **/notes.txt -CHANGELOG.md -README.md yarn.lock test/ +src/ +scripts/ +typings/ **/.trash .nyc_output/ .vscode/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..bbaf155 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: true, + trailingComma: 'all', + singleQuote: true, + printWidth: 120, + tabWidth: 2, +}; diff --git a/.travis.yml b/.travis.yml index 2ad6c68..1dc9bdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,11 @@ branches: install: yarn install script: - - npm test + - npm run build + - npm run test:unit after_success: - - npm run coveralls + # - npm run coveralls cache: yarn: true diff --git a/index.js b/index.js deleted file mode 100644 index 411f6f9..0000000 --- a/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Export lib/gstore-node - * - */ - -'use strict'; - -module.exports = require('./lib/'); diff --git a/lib/constants.js b/lib/constants.js deleted file mode 100644 index ced1695..0000000 --- a/lib/constants.js +++ /dev/null @@ -1,11 +0,0 @@ - -'use strict'; - -const queries = { - formats: { - JSON: 'JSON', - ENTITY: 'ENTITY', - }, -}; - -module.exports = { queries }; diff --git a/lib/dataloader.js b/lib/dataloader.js deleted file mode 100644 index 8974d20..0000000 --- a/lib/dataloader.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -/* eslint-disable import/no-extraneous-dependencies */ - -const optional = require('optional'); -const dsAdapter = require('nsql-cache-datastore')(); -const arrify = require('arrify'); - -const DataLoader = optional('dataloader'); -const { keyToString } = dsAdapter; - -/** - * Create a DataLoader instance - * @param {Datastore} ds @google-cloud Datastore instance - */ -function createDataLoader(ds) { - ds = typeof ds !== 'undefined' ? ds : this && this.ds; - - if (!ds) { - throw new Error('A Datastore instance has to be passed'); - } - - return new DataLoader(keys => ( - ds.get(keys).then(([res]) => { - // When providing an Array with 1 Key item, google-datastore - // returns a single item. - // For predictable results in gstore, all responses from Datastore.get() - // calls return an Array - const entities = arrify(res); - const entitiesByKey = {}; - entities.forEach(entity => { - entitiesByKey[keyToString(entity[ds.KEY])] = entity; - }); - - return keys.map(key => entitiesByKey[keyToString(key)] || null); - }) - ), { - cacheKeyFn: _key => keyToString(_key), - maxBatchSize: 1000, - }); -} - -module.exports = { createDataLoader }; diff --git a/lib/entity.js b/lib/entity.js deleted file mode 100644 index 0a4be95..0000000 --- a/lib/entity.js +++ /dev/null @@ -1,482 +0,0 @@ - -'use strict'; - -const is = require('is'); -const hooks = require('promised-hooks'); -const arrify = require('arrify'); - -const datastoreSerializer = require('./serializer').Datastore; -const defaultValues = require('./helpers/defaultValues'); -const { errorCodes } = require('./errors'); -const { validation, populateHelpers } = require('./helpers'); - -const { populateFactory } = populateHelpers; - -class Entity { - constructor(data, id, ancestors, namespace, key) { - this.className = 'Entity'; - this.schema = this.constructor.schema; - this.excludeFromIndexes = {}; - - /** - * Object to store custom data for the entity. - * In some cases we might want to add custom data onto the entity - * and as Typescript won't allow random properties to be added, this - * is the place to add data based on the context. - */ - this.context = {}; - - if (key) { - if (!this.gstore.ds.isKey(key)) { - throw new Error('Entity Key must be a Datastore Key'); - } - this.entityKey = key; - } else { - this.entityKey = createKey(this, id, ancestors, namespace); - } - - this.setId(); - - // create entityData from data provided - this.____buildEntityData(data || {}); - - /** - * Create virtual properties (getters and setters for entityData object) - */ - Object.keys(this.schema.paths) - .filter(pathKey => ({}.hasOwnProperty.call(this.schema.paths, pathKey))) - .forEach(pathKey => Object.defineProperty(this, pathKey, { - get: function getProp() { return this.entityData[pathKey]; }, - set: function setProp(newValue) { - this.entityData[pathKey] = newValue; - }, - })); - - // wrap entity with hook methods - hooks.wrap(this); - - // add middleware defined on Schena - registerHooksFromSchema(this); - } - - save(transaction, opts = {}) { - this.__hooksEnabled = true; - const _this = this; - const options = { - method: 'upsert', - sanitizeEntityData: true, - ...opts, - }; - - // Sanitize - if (options.sanitizeEntityData) { - this.entityData = this.constructor.sanitize.call( - this.constructor, this.entityData, { disabled: ['write'] } - ); - } - - // Validate - const { error } = validate(); - if (error) { - return Promise.reject(error); - } - - this.entityData = prepareData.call(this); - - const entity = datastoreSerializer.toDatastore(this); - entity.method = options.method; - - if (transaction) { - if (transaction.constructor.name !== 'Transaction') { - return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); - } - - addPostHooksTransaction.call(this); - transaction.save(entity); - - return Promise.resolve(this); - } - - return this.gstore.ds.save(entity).then(onSuccess); - - // -------------------------- - - function onSuccess() { - /** - * Make sure to clear the cache for this Entity Kind - */ - if (_this.constructor.__hasCache(options)) { - return _this.constructor.clearCache() - .then(() => _this) - .catch(err => { - let msg = 'Error while clearing the cache after saving the entity.'; - msg += 'The entity has been saved successfully though. '; - msg += 'Both the cache error and the entity saved have been attached.'; - const cacheError = new Error(msg); - cacheError.__entity = _this; - cacheError.__cacheError = err; - throw cacheError; - }); - } - - _this.setId(); - - return _this; - } - - function validate() { - let { error: err } = validateEntityData(); - - if (!err) { - ({ error: err } = validateMethod(options.method)); - } - - return { error: err }; - } - - function validateEntityData() { - if (_this.schema.options.validateBeforeSave) { - return _this.validate(); - } - - return { error: null }; - } - - function validateMethod(method) { - const allowed = { - update: true, - insert: true, - upsert: true, - }; - - return !allowed[method] - ? { error: new Error('Method must be either "update", "insert" or "upsert"') } - : { error: null }; - } - - /** - * Process some basic formatting to the entity data before save - * - automatically set the modifiedOn property to current date (if exists on schema) - * - convert object with latitude/longitude to Datastore GeoPoint - */ - function prepareData() { - updateModifiedOn.call(this); - convertGeoPoints.call(this); - - return this.entityData; - - //-------------------------- - - /** - * If the schema has a "modifiedOn" property we automatically - * update its value to the current dateTime - */ - function updateModifiedOn() { - if ({}.hasOwnProperty.call(this.schema.paths, 'modifiedOn')) { - this.entityData.modifiedOn = new Date(); - } - } - - /** - * If the entityData has some property of type 'geoPoint' - * and its value is an js object with "latitude" and "longitude" - * we convert it to a datastore GeoPoint. - */ - function convertGeoPoints() { - if (!{}.hasOwnProperty.call(this.schema.__meta, 'geoPointsProps')) { - return; - } - - this.schema.__meta.geoPointsProps.forEach(property => { - if ({}.hasOwnProperty.call(_this.entityData, property) - && _this.entityData[property] !== null - && _this.entityData[property].constructor.name !== 'GeoPoint') { - _this.entityData[property] = _this.gstore.ds.geoPoint(_this.entityData[property]); - } - }); - } - } - - /** - * If it is a transaction, we create a hooks.post array that will be executed - * when transaction succeeds by calling transaction.execPostHooks() (returns a Promises) - */ - function addPostHooksTransaction() { - // disable (post) hooks, we will only trigger them on transaction succceed - this.__hooksEnabled = false; - this.constructor.hooksTransaction.call( - _this, - transaction, - this.__posts - ? this.__posts.save - : undefined - ); - } - } - - validate() { - const { - entityData, schema, entityKind, gstore, - } = this; - - return validation.validate( - entityData, - schema, - entityKind, - gstore.ds - ); - } - - plain(options) { - options = typeof options === 'undefined' ? {} : options; - - if (typeof options !== 'undefined' && !is.object(options)) { - throw new Error('Options must be an Object'); - } - const readAll = !!options.readAll || false; - const virtuals = !!options.virtuals || false; - const showKey = !!options.showKey || false; - - if (virtuals) { - this.entityData = this.getEntityDataWithVirtuals(); - } - - const data = datastoreSerializer.fromDatastore.call(this, this.entityData, { readAll, showKey }); - - return data; - } - - get(path) { - if ({}.hasOwnProperty.call(this.schema.virtuals, path)) { - return this.schema.virtuals[path].applyGetters(this.entityData); - } - return this.entityData[path]; - } - - set(path, value) { - if ({}.hasOwnProperty.call(this.schema.virtuals, path)) { - return this.schema.virtuals[path].applySetters(value, this.entityData); - } - - this.entityData[path] = value; - return this; - } - - setId() { - this.id = this.entityKey.id || this.entityKey.name; - } - - /** - * Return a Model from Gstore - * @param name : model name - */ - model(name) { - return this.constructor.gstore.model(name); - } - - /** - * Fetch entity from Datastore - * - * @param {Function} cb Callback - */ - datastoreEntity(options = {}) { - const _this = this; - - if (this.constructor.__hasCache(options)) { - return this.gstore.cache.keys - .read(this.entityKey, options) - .then(onSuccess); - } - return this.gstore.ds.get(this.entityKey).then(onSuccess); - - // ------------------------ - - function onSuccess(result) { - const datastoreEntity = result ? result[0] : null; - - if (!datastoreEntity) { - if (_this.gstore.config.errorOnEntityNotFound) { - const error = new Error('Entity not found'); - error.code = errorCodes.ERR_ENTITY_NOT_FOUND; - throw error; - } - - return null; - } - - _this.entityData = datastoreEntity; - return _this; - } - } - - populate(path, propsToSelect) { - const refsToPopulate = []; - - const promise = Promise.resolve(this) - .then(this.constructor.populate(refsToPopulate)); - - promise.populate = populateFactory(refsToPopulate, promise, this.constructor); - promise.populate(path, propsToSelect); - return promise; - } - - getEntityDataWithVirtuals() { - const { virtuals } = this.schema; - const entityData = { ...this.entityData }; - - Object.keys(virtuals).forEach(k => { - if ({}.hasOwnProperty.call(entityData, k)) { - virtuals[k].applySetters(entityData[k], entityData); - } else { - virtuals[k].applyGetters(entityData); - } - }); - - return entityData; - } - - ____buildEntityData(data) { - const { schema } = this; - const isJoiSchema = schema.isJoi; - - // If Joi schema, get its default values - if (isJoiSchema) { - const { error, value } = schema.validateJoi(data); - - if (!error) { - this.entityData = { ...value }; - } - } - - this.entityData = { ...this.entityData, ...data }; - - let isArray; - let isObject; - - Object.entries(schema.paths).forEach(([key, prop]) => { - const hasValue = {}.hasOwnProperty.call(this.entityData, key); - const isOptional = {}.hasOwnProperty.call(prop, 'optional') && prop.optional !== false; - const isRequired = {}.hasOwnProperty.call(prop, 'required') && prop.required === true; - - // Set Default Values - if (!isJoiSchema && !hasValue && !isOptional) { - let value = null; - - if ({}.hasOwnProperty.call(prop, 'default')) { - if (typeof prop.default === 'function') { - value = prop.default(); - } else { - value = prop.default; - } - } - - if (({}).hasOwnProperty.call(defaultValues.__map__, value)) { - /** - * If default value is in the gstore.defaultValue hashTable - * then execute the handler for that shortcut - */ - value = defaultValues.__handler__(value); - } else if (value === null && {}.hasOwnProperty.call(prop, 'values') && !isRequired) { - // Default to first value of the allowed values if **not** required - [value] = prop.values; - } - - this.entityData[key] = value; - } - - // Set excludeFromIndexes - // ---------------------- - isArray = prop.type === Array || (prop.joi && prop.joi._type === 'array'); - isObject = prop.type === Object || (prop.joi && prop.joi._type === 'object'); - - if (prop.excludeFromIndexes === true) { - if (isArray) { - // We exclude both the array values + all the child properties of object items - this.excludeFromIndexes[key] = [`${key}[]`, `${key}[].*`]; - } else if (isObject) { - // We exclude the emmbeded entity + all its properties - this.excludeFromIndexes[key] = [key, `${key}.*`]; - } else { - this.excludeFromIndexes[key] = [key]; - } - } else if (prop.excludeFromIndexes !== false) { - const excludedArray = arrify(prop.excludeFromIndexes); - if (isArray) { - // The format to exclude a property from an embedded entity inside - // an array is: "myArrayProp[].embeddedKey" - this.excludeFromIndexes[key] = excludedArray.map(propExcluded => `${key}[].${propExcluded}`); - } else if (isObject) { - // The format to exclude a property from an embedded entity - // is: "myEmbeddedEntity.key" - this.excludeFromIndexes[key] = excludedArray.map(propExcluded => `${key}.${propExcluded}`); - } - } - }); - - // add Symbol Key to the entityData - this.entityData[this.gstore.ds.KEY] = this.entityKey; - } -} - -// Private -// ------- -function createKey(self, id, ancestors, namespace) { - if (id && !is.number(id) && !is.string(id)) { - throw new Error('id must be a string or a number'); - } - - const hasAncestors = typeof ancestors !== 'undefined' && ancestors !== null && is.array(ancestors); - - /* - /* Create copy of ancestors to avoid mutating the Array - */ - if (hasAncestors) { - ancestors = ancestors.slice(); - } - - let path; - if (id) { - path = hasAncestors ? ancestors.concat([self.entityKind, id]) : [self.entityKind, id]; - } else { - if (hasAncestors) { - ancestors.push(self.entityKind); - } - path = hasAncestors ? ancestors : self.entityKind; - } - - if (namespace && !is.array(path)) { - path = [path]; - } - return namespace ? self.gstore.ds.key({ namespace, path }) : self.gstore.ds.key(path); -} - -function registerHooksFromSchema(self) { - const callQueue = self.schema.callQueue.entity; - - if (!Object.keys(callQueue).length) { - return self; - } - - Object.keys(callQueue).forEach(addHooks); - - // --------------------------------------- - - function addHooks(method) { - if (!self[method]) { - return; - } - - // Add Pre hooks - callQueue[method].pres.forEach(fn => { - self.pre(method, fn); - }); - - // Add Pre hooks - callQueue[method].post.forEach(fn => { - self.post(method, fn); - }); - } - return self; -} - -module.exports = Entity; diff --git a/lib/errors.js b/lib/errors.js deleted file mode 100755 index 84317bd..0000000 --- a/lib/errors.js +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable max-classes-per-file, no-use-before-define */ - -'use strict'; - -const util = require('util'); -const is = require('is'); - -const errorCodes = { - ERR_ENTITY_NOT_FOUND: 'ERR_ENTITY_NOT_FOUND', - ERR_GENERIC: 'ERR_GENERIC', - ERR_VALIDATION: 'ERR_VALIDATION', - ERR_PROP_TYPE: 'ERR_PROP_TYPE', - ERR_PROP_VALUE: 'ERR_PROP_VALUE', - ERR_PROP_NOT_ALLOWED: 'ERR_PROP_NOT_ALLOWED', - ERR_PROP_REQUIRED: 'ERR_PROP_REQUIRED', - ERR_PROP_IN_RANGE: 'ERR_PROP_IN_RANGE', -}; - -const message = (text, ...args) => util.format(text, ...args); - -const messages = { - ERR_GENERIC: 'An error occured', - ERR_VALIDATION: entityKind => message('The entity data does not validate against the "%s" Schema', entityKind), - ERR_PROP_TYPE: (prop, type) => message('Property "%s" must be a %s', prop, type), - ERR_PROP_VALUE: (value, prop) => message('"%s" is not a valid value for property "%s"', value, prop), - ERR_PROP_NOT_ALLOWED: (prop, entityKind) => ( - message('Property "%s" is not allowed for entityKind "%s"', prop, entityKind) - ), - ERR_PROP_REQUIRED: prop => message('Property "%s" is required but no value has been provided', prop), - ERR_PROP_IN_RANGE: (prop, range) => message('Property "%s" must be one of [%s]', prop, range && range.join(', ')), -}; - -class GstoreError extends Error { - constructor(code, msg, args) { - if (!msg && code && code in messages) { - if (is.function(messages[code])) { - msg = messages[code](...args.messageParams); - } else { - msg = messages[code]; - } - } - - if (!msg) { - msg = messages.ERR_GENERIC; - } - - super(msg, code, args); - this.name = 'GstoreError'; - this.message = msg; - this.code = code || errorCodes.ERR_GENERIC; - - if (args) { - Object.keys(args).forEach(k => { - if (k !== 'messageParams') { - this[k] = args[k]; - } - }); - } - - Error.captureStackTrace(this, this.constructor); - } - - static get TypeError() { - return class extends TypeError {}; - } - - static get ValueError() { - return class extends ValueError {}; - } - - static get ValidationError() { - return class extends ValidationError {}; - } -} - -class ValidationError extends GstoreError { - constructor(...args) { - super(...args); - this.name = 'ValidationError'; - Error.captureStackTrace(this, this.constructor); - } -} - -class TypeError extends GstoreError { - constructor(...args) { - super(...args); - this.name = 'TypeError'; - Error.captureStackTrace(this, this.constructor); - } -} - -class ValueError extends GstoreError { - constructor(...args) { - super(...args); - this.name = 'ValueError'; - Error.captureStackTrace(this, this.constructor); - } -} - -module.exports = { - GstoreError, - ValidationError, - TypeError, - ValueError, - message, - errorCodes, -}; diff --git a/lib/helpers/defaultValues.js b/lib/helpers/defaultValues.js deleted file mode 100644 index 0f14279..0000000 --- a/lib/helpers/defaultValues.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const NOW = 'CURRENT_DATETIME'; -const timeNow = () => new Date(); -const map = { - CURRENT_DATETIME: timeNow, -}; - -const handler = value => { - if (({}).hasOwnProperty.call(map, value)) { - return map[value](); - } - - return null; -}; - -module.exports = { - NOW, - __handler__: handler, - __map__: map, -}; diff --git a/lib/helpers/index.js b/lib/helpers/index.js deleted file mode 100644 index d81072a..0000000 --- a/lib/helpers/index.js +++ /dev/null @@ -1,14 +0,0 @@ - -'use strict'; - -const queryHelpers = require('./queryhelpers'); -const validation = require('./validation'); -const populateHelpers = require('./populateHelpers'); -// const googleCloud = require('./google-cloud'); - -module.exports = { - queryHelpers, - validation, - populateHelpers, - // googleCloud, -}; diff --git a/lib/helpers/populateHelpers.js b/lib/helpers/populateHelpers.js deleted file mode 100644 index 3fa47be..0000000 --- a/lib/helpers/populateHelpers.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -const arrify = require('arrify'); - -/** - * - * @param {*} initialPath Path to add to the refs array - * @param {*} select Array of properties to select from the populated ref - * @param {*} refs Array of refs, each index is one level deep in the entityData tree - * - * @example - * - * const entityData = { - * user: Key, // ---> once fetched it will be { name: String, company: Key } - * address: Key - * } - * - * To fetch the "address", the "user" and the user's "conmpany", the array of refs - * to retrieve will have the following shape - * - * [ - * [{ path: 'user', select: ['*'] }, [ path: 'address', select: ['*'] ], // tree depth at level 0 - * [{ path: 'user.company', select: ['*'] }], // tree depth at level 1 (will be fetched after level 0 has been fetched) - * ] - */ -const addPathToPopulateRefs = (initialPath, _select = ['*'], refs) => { - const pathToArray = initialPath.split('.'); - const select = arrify(_select); - let prefix = ''; - - pathToArray.forEach((prop, i) => { - const currentPath = prefix ? `${prefix}.${prop}` : prop; - const nextPath = pathToArray[i + 1]; - const hasNextPath = typeof nextPath !== 'undefined'; - const refsAtCurrentTreeLevel = refs[i] || []; - - // Check if we alreday have a config for this tree level - const pathConfig = refsAtCurrentTreeLevel.find(ref => ref.path === currentPath); - - if (!pathConfig) { - refsAtCurrentTreeLevel.push({ path: currentPath, select: hasNextPath ? [nextPath] : select }); - } else if (hasNextPath && !pathConfig.select.some(s => s === nextPath)) { - // Add the next path to the selected properties on the ref - pathConfig.select.push(nextPath); - } else if (!hasNextPath && select.length) { - pathConfig.select.push(...select); - } - refs[i] = refsAtCurrentTreeLevel; - - prefix = currentPath; - }); -}; - -const populateFactory = (refsToPopulate, promise, Model) => (path, propsToSelect) => { - if (propsToSelect && Array.isArray(path)) { - throw new Error('Only 1 property can be populated when fields to select are provided'); - } - - // If no path is specified, we fetch all the schema properties that are references to entities (Keys) - const paths = path - ? arrify(path) - : Model.getEntitiesRefsFromSchema(); - - paths.forEach(p => addPathToPopulateRefs(p, propsToSelect, refsToPopulate)); - - return promise; -}; - -module.exports = { addPathToPopulateRefs, populateFactory }; diff --git a/lib/helpers/queryhelpers.js b/lib/helpers/queryhelpers.js deleted file mode 100644 index fb67e31..0000000 --- a/lib/helpers/queryhelpers.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -const is = require('is'); - -function buildFromOptions(query, options, ds) { - if (!query || query.constructor.name !== 'Query') { - throw new Error('Query not passed'); - } - - if (!options || typeof options !== 'object') { - return query; - } - - if (options.limit) { - query.limit(options.limit); - } - - if (options.offset) { - query.offset(options.offset); - } - - if (options.order) { - if (!options.order.length) { - options.order = [options.order]; - } - options.order.forEach(order => { - query.order(order.property, { - descending: {}.hasOwnProperty.call(order, 'descending') ? order.descending : false, - }); - }); - } - - if (options.select) { - query.select(options.select); - } - - if (options.ancestors) { - if (!ds || ds.constructor.name !== 'Datastore') { - throw new Error('Datastore instance not passed'); - } - - const ancestorKey = options.namespace - ? ds.key({ namespace: options.namespace, path: options.ancestors.slice() }) - : ds.key(options.ancestors.slice()); - - query.hasAncestor(ancestorKey); - } - - if (options.filters) { - if (!is.array(options.filters)) { - throw new Error('Wrong format for filters option'); - } - - if (!is.array(options.filters[0])) { - options.filters = [options.filters]; - } - - if (options.filters[0].length > 1) { - options.filters.forEach(filter => { - // We check if the value is a function - // if it is, we execute it. - let value = filter[filter.length - 1]; - value = is.fn(value) ? value() : value; - const f = filter.slice(0, -1).concat([value]); - - query.filter.apply(query, f); - }); - } - } - - if (options.start) { - query.start(options.start); - } - - return query; -} - -module.exports = { - buildFromOptions, -}; diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js deleted file mode 100755 index 3509980..0000000 --- a/lib/helpers/validation.js +++ /dev/null @@ -1,324 +0,0 @@ -'use strict'; - -const moment = require('moment'); -const validator = require('validator'); -const is = require('is'); - -const gstoreErrors = require('../errors'); - -const isValidDate = value => { - if (value.constructor.name !== 'Date' - && (typeof value !== 'string' - || !value.match(/\d{4}-\d{2}-\d{2}([ ,T])?(\d{2}:\d{2}:\d{2})?(\.\d{1,3})?/) - || !moment(value).isValid())) { - return false; - } - return true; -}; - -const isInt = n => Number(n) === n && n % 1 === 0; - -const isFloat = n => Number(n) === n && n % 1 !== 0; - -const isValueEmpty = v => ( - v === null || v === undefined || (typeof v === 'string' && v.trim().length === 0) -); - -const isValidLngLat = data => { - const validLatitude = (isInt(data.latitude) || isFloat(data.latitude)) - && data.latitude >= -90 && data.latitude <= 90; - const validLongitude = (isInt(data.longitude) || isFloat(data.longitude)) - && data.longitude >= -180 && data.longitude <= 180; - - return validLatitude && validLongitude; -}; - -const errorToObject = ({ - code, type, message, ref, -}) => ({ - code, - type, - message, - ref, -}); - -const validatePropType = (value, propType, prop, pathConfig, datastore) => { - let valid; - let ref; - let type = propType; - if (typeof propType === 'function') { - type = propType.name.toLowerCase(); - } - - switch (type) { - case 'entityKey': - valid = datastore.isKey(value); - ref = 'key.base'; - if (valid && pathConfig.ref) { - // Make sure the Entity Kind is also valid (if any) - const entityKind = value.path[value.path.length - 2]; - valid = entityKind === pathConfig.ref; - ref = 'key.entityKind'; - } - break; - case 'string': - /* eslint valid-typeof: "off" */ - valid = typeof value === 'string'; - ref = 'string.base'; - break; - case 'date': - valid = isValidDate(value); - ref = 'datetime.base'; - break; - case 'array': - valid = is.array(value); - ref = 'array.base'; - break; - case 'number': { - const isIntInstance = value.constructor.name === 'Int'; - if (isIntInstance) { - valid = !isNaN(parseInt(value.value, 10)); - } else { - valid = isInt(value); - } - ref = 'int.base'; - break; - } - case 'double': { - const isIntInstance = value.constructor.name === 'Double'; - if (isIntInstance) { - valid = isFloat(parseFloat(value.value, 10)) - || isInt(parseFloat(value.value, 10)); - } else { - valid = isFloat(value) || isInt(value); - } - ref = 'double.base'; - break; - } - case 'buffer': - valid = value instanceof Buffer; - ref = 'buffer.base'; - break; - case 'geoPoint': { - if (is.object(value) && Object.keys(value).length === 2 - && {}.hasOwnProperty.call(value, 'longitude') - && {}.hasOwnProperty.call(value, 'latitude')) { - valid = isValidLngLat(value); - } else { - valid = value.constructor.name === 'GeoPoint'; - } - ref = 'geopoint.base'; - break; - } - default: - /* eslint valid-typeof: "off" */ - if (Array.isArray(value)) { - valid = false; - } else { - valid = typeof value === type; - } - ref = 'prop.type'; - } - - if (!valid) { - return new gstoreErrors.TypeError( - gstoreErrors.errorCodes.ERR_PROP_TYPE, - null, - { ref, messageParams: [prop, type], property: prop } - ); - } - - return null; -}; - -const validatePropValue = (value, validationRule, propType, prop) => { - let validationArgs = []; - let validationFn; - - /** - * If the validate property is an object, then it's assumed that - * it contains the 'rule' property, which will be the new - * validationRule's value. - * If the 'args' prop was passed then we concat them to 'validationArgs'. - */ - - if (typeof validationRule === 'object') { - const { rule } = validationRule; - validationArgs = validationRule.args || []; - - if (typeof rule === 'function') { - validationFn = rule; - validationArgs = [value, validator, ...validationArgs]; - } else { - validationRule = rule; - } - } - - if (!validationFn) { - /** - * Validator.js only works with string values - * let's make sure we are working with a string. - */ - const isObject = typeof value === 'object'; - const strValue = typeof value !== 'string' && !isObject ? String(value) : value; - validationArgs = [strValue, ...validationArgs]; - - validationFn = validator[validationRule]; - } - - if (!validationFn.apply(validator, validationArgs)) { - return new gstoreErrors.ValueError( - gstoreErrors.errorCodes.ERR_PROP_VALUE, - null, - { type: 'prop.validator', messageParams: [value, prop], property: prop } - ); - } - - return null; -}; - -const validate = (entityData, schema, entityKind, datastore) => { - const errors = []; - - let prop; - let skip; - let schemaHasProperty; - let pathConfig; - let propertyType; - let propertyValue; - let isEmpty; - let isRequired; - let error; - - const props = Object.keys(entityData); - const totalProps = Object.keys(entityData).length; - - if (schema.isJoi) { - // We leave the validation to Joi - return schema.validateJoi(entityData); - } - - for (let i = 0; i < totalProps; i += 1) { - prop = props[i]; - skip = false; - error = null; - schemaHasProperty = {}.hasOwnProperty.call(schema.paths, prop); - pathConfig = schema.paths[prop] || {}; - propertyType = schemaHasProperty ? pathConfig.type : null; - propertyValue = entityData[prop]; - isEmpty = isValueEmpty(propertyValue); - - if (typeof propertyValue === 'string') { - propertyValue = propertyValue.trim(); - } - - if ({}.hasOwnProperty.call(schema.virtuals, prop)) { - // Virtual, remove it and skip the rest - delete entityData[prop]; - skip = true; - } else if (!schemaHasProperty && schema.options.explicitOnly === false) { - // No more validation, key does not exist but it is allowed - skip = true; - } - - if (!skip) { - // ... is allowed? - if (!schemaHasProperty) { - error = new gstoreErrors.ValidationError( - gstoreErrors.errorCodes.ERR_PROP_NOT_ALLOWED, - null, - { - type: 'prop.not.allowed', - messageParams: [prop, entityKind], - property: prop, - } - ); - errors.push(errorToObject(error)); - // return; - } - - // ...is required? - isRequired = schemaHasProperty - && {}.hasOwnProperty.call(pathConfig, 'required') - && pathConfig.required === true; - - if (isRequired && isEmpty) { - error = new gstoreErrors.ValueError( - gstoreErrors.errorCodes.ERR_PROP_REQUIRED, - null, - { - type: 'prop.required', - messageParams: [prop], - property: prop, - } - ); - errors.push(errorToObject(error)); - } - - // ... is valid prop Type? - if (schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(pathConfig, 'type')) { - error = validatePropType(propertyValue, propertyType, prop, pathConfig, datastore); - - if (error) { - errors.push(errorToObject(error)); - } - } - - // ... is valid prop Value? - if (error === null - && schemaHasProperty - && !isEmpty - && {}.hasOwnProperty.call(pathConfig, 'validate')) { - error = validatePropValue(propertyValue, pathConfig.validate, propertyType, prop); - if (error) { - errors.push(errorToObject(error)); - } - } - - // ... is value in range? - if (schemaHasProperty && !isEmpty - && {}.hasOwnProperty.call(pathConfig, 'values') - && pathConfig.values.indexOf(propertyValue) < 0) { - error = new gstoreErrors.ValueError( - gstoreErrors.errorCodes.ERR_PROP_IN_RANGE, - null, - { type: 'value.range', messageParams: [prop, pathConfig.values], property: prop } - ); - - errors.push(errorToObject(error)); - } - } - } - - let validationError = null; - - if (Object.keys(errors).length > 0) { - validationError = new gstoreErrors.ValidationError( - gstoreErrors.errorCodes.ERR_VALIDATION, - null, - { errors, messageParams: [entityKind] } - ); - } - - return { - error: validationError, - value: entityData, - then: (onSuccess, onError) => { - if (validationError) { - return Promise.resolve(onError(validationError)); - } - - return Promise.resolve(onSuccess(entityData)); - }, - catch: onError => { - if (validationError) { - return Promise.resolve(onError(validationError)); - } - return undefined; - }, - }; -}; - -module.exports = { - validate, -}; diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index 1c2f044..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,1011 +0,0 @@ -// Type definitions for gstore-node v5.0.0 -// Project: [gstore-node] -// Definitions by: [Sébastien Loix] <[http://s.loix.me]> - -/// - -import { Datastore } from "@google-cloud/datastore"; -import { entity } from "@google-cloud/datastore/build/src/entity"; -import { - Query as GoogleDatastoreQuery, - Operator -} from "@google-cloud/datastore/build/src/query"; -import { Transaction } from "@google-cloud/datastore/build/src/transaction"; - -/** - * gstore-node Instance - * - * @class Gstore - */ -export class Gstore { - constructor(config?: GstoreConfig); - - /** - * The unerlying google-cloud Datastore instance - */ - ds: any; - - /** - * The underlying gstore-cache instance - * - * @type {GstoreCache} - */ - cache: GstoreCache.Cache; - - Schema: typeof Schema; - - /** - * Default Values helpers for Schemas - * - * @type {{ NOW: string }} - */ - defaultValues: { NOW: string }; - - errors: { - codes: ErrorCodes; - GstoreError: typeof GstoreError; - ValidationError: typeof ValidationError; - TypeError: typeof TypeError; - ValueError: typeof ValueError; - }; - - /** - * Initialize a @google-cloud/datastore Transaction - */ - transaction(): any; - - /** - * Connect gstore node to the Datastore instance - * - * @param {Datastore} datastore A Datastore instance - */ - connect(datastore: Datastore): void; - - /** - * Initalize a gstore-node Model - * - * @param {string} entityKind The Google Entity Kind - * @returns {Model} A gstore Model - */ - model( - entityKind: string, - schema?: Schema - ): Model; - - /** - * Create a DataLoader instance - * - * @returns {DataLoader} The DataLoader instance - * @link https://sebelga.gitbooks.io/gstore-node/content/dataloader.html#dataloader - */ - createDataLoader(): DataLoader; - - /** - * This method is an alias of the underlying @google-cloud/datastore save() method, with the exception that you can pass it an Entity instance or an Array of entities instances and this method will first convert the instances to the correct Datastore format before saving. - * - * @param {(Entity | Entity[])} entity The entity(ies) to delete (any Entity Kind). Can be one or many (Array). - * @param {Transaction} [transaction] An Optional transaction to save the entities into - * @returns {Promise} - */ - save( - entity: Entity | Entity[], - transaction?: Transaction, - options?: { - validate?: boolean; - method?: "insert" | "update" | "upsert"; - } - ): Promise; -} - -/** - * gstore-node Schema - * - * @class Schema - */ -export class Schema { - constructor( - properties: { [P in keyof T]: SchemaPathDefinition }, - options?: SchemaOptions - ); - - /** - * Custom Schema Types - */ - static Types: { - Double: "double"; - GeoPoint: "geoPoint"; - Key: "entityKey"; - }; - - /** - * Schema paths - * - * @type {{ [propName: string]: SchemaPathDefinition }} - */ - readonly paths: { [P in keyof T]: SchemaPathDefinition }; - /** - * Add custom methods to entities. - * @link https://sebelga.gitbooks.io/gstore-node/content/schema/custom-methods.html - * - * @example - * ``` - * schema.methods.profilePict = function() { - return this.model('Image').get(this.imgIdx) - * } - * ``` - */ - readonly methods: { [propName: string]: (...args: any[]) => any }; - - /** - * Getter / Setter for Schema paths. - * - * @param {string} propName The entity property - * @param {SchemaPathDefinition} [definition] The property definition - * @link https://sebelga.gitbooks.io/gstore-node/content/schema/schema-methods/path.html - */ - path(propName: string, definition?: SchemaPathDefinition): void; - - /** - * Getter / Setter of a virtual property. - * Virtual properties are created dynamically and not saved in the Datastore. - * - * @param {string} propName The virtual property name - * @link https://sebelga.gitbooks.io/gstore-node/content/schema/schema-methods/virtual.html - */ - virtual( - propName: string - ): { - get(cb: () => any): void; - set(cb: (propName: string) => void): void; - }; - - queries(shortcutQuery: "list", options: QueryListOptions): void; - - /** - * Register a middleware to be executed before "save()", "delete()", "findOne()" or any of your custom method. The callback will receive the original argument(s) passed to the target method. You can modify them in your resolve passing an object with an __override property containing the new parameter(s) for the target method. - * - * @param {string} method The target method to add the hook to - * @param {(...args: any[]) => Promise} callback Function to execute before the target method. It must return a Promise - * @link https://sebelga.gitbooks.io/gstore-node/content/middleware-hooks/pre-hooks.html - */ - pre( - method: string, - callback: funcReturningPromise | funcReturningPromise[] - ): void; - - /** - * Register a "post" middelware to execute after a target method. - * - * @param {string} method The target method to add the hook to - * @param {(response: any) => Promise} callback Function to execute after the target method. It must return a Promise - * @link https://sebelga.gitbooks.io/gstore-node/content/middleware-hooks/post-hooks.html - */ - post( - method: string, - callback: funcReturningPromise | funcReturningPromise[] - ): void; - - /** - * Executes joi.validate on given data. If schema does not have a joi config object data is returned - * - * @param {*} data The data to sanitize - * @returns {*} The data sanitized - */ - validateJoi(data: { [propName: string]: any }): Validation<{ [P in keyof T]: T[P] }>; - - /** - * Checks if the schema has a joi config object. - */ - readonly isJoi: boolean; -} - -export interface Model { - /** - * gstore-node instance - * - * @static - * @type {Gstore} - */ - gstore: Gstore; - - /** - * The Model Schema - * - * @static - * @type {Schema} - */ - schema: Schema; - - /** - * The Model Datastore Entity Kind - * - * @static - * @type {string} - */ - entityKind: string; - - /** - * Creates an entity, instance of the Model. - * @param {{ [propName: string]: any }} data The entity data - * @param {(string | number)} [id] An optional entity id or name. If not provided it will be automatically generated. - * @param {(Array)} [ancestors] The entity Ancestors - * @param {string} [namespace] The entity Namespace - */ - new ( - data: { [P in keyof T]: T[P] }, - id?: string | number, - ancestors?: Array, - namespace?: string - ): Entity; - - /** - * Fetch an Entity by KEY from the Datastore - * - * @param {(string | number | string[] | number[])} id The entity ID - * @param {(Array)} [ancestors] The entity Ancestors - * @param {string} [namespace] The entity Namespace - * @param {*} [transaction] The current Datastore Transaction (if any) - * @param {({ preserveOrder: boolean, dataloader: any, cache: boolean, ttl: number | { [propName: string] : number } })} [options] Additional configuration - * @returns {Promise} The entity fetched from the Datastore - * @link https://sebelga.gitbooks.io/gstore-node/content/model/get.html - */ - get>( - id: U, - ancestors?: Array, - namespace?: string, - transaction?: Transaction, - options?: { - /** - * If you have provided an Array of ids, the order returned by the Datastore is not guaranteed. If you need the entities back in the same order of the IDs provided, then set `preserveOrder: true` - * - * @type {boolean} - * @default false - */ - preserveOrder?: boolean; - /** - * An optional Dataloader instance. - * - * @type {*} - * @link https://sebelga.gitbooks.io/gstore-node/content/dataloader.html#dataloader - */ - dataloader?: any; - /** - * Only if the cache has been activated. - * Fetch the entity from the cache first. If you want to bypass the cache and go to the Datastore directly, set `cache: false`. - * - * @type {boolean} - * @default The "global" cache configuration - * @link https://sebelga.gitbooks.io/gstore-node/content/cache.html - */ - cache?: boolean; - /** - * Only if the cache has been activated. - * After the entty has been fetched from the Datastore it will be added to the cache. You can specify here a custom ttl (Time To Live) for the entity. - * - * @type {(number | { [propName: string] : number })} - * @default The "ttl.keys" cache configuration - * @link https://sebelga.gitbooks.io/gstore-node/content/cache.html - */ - ttl?: number | { [propName: string]: number }; - } - ): PromiseWithPopulate< - U extends Array ? Entity[] : Entity - >; - - /** - * Update an Entity in the Datastore - * - * @static - * @param {(string | number)} id Entity id or name - * @param {*} data The data to update (it will be merged with the data in the Datastore unless options.replace is set to "true") - * @param {(Array)} [ancestors] The entity Ancestors - * @param {string} [namespace] The entity Namespace - * @param {*} [transaction] The current transaction (if any) - * @param {{ dataloader?: any, replace?: boolean }} [options] Additional configuration - * @returns {Promise} The entity updated in the Datastore - * @link https://sebelga.gitbooks.io/gstore-node/content/model/update-method.html - */ - update( - id: string | number, - data: any, - ancestors?: Array, - namespace?: string, - transaction?: Transaction, - options?: { - /** - * An optional Dataloader instance. - * - * @type {*} - * @link https://sebelga.gitbooks.io/gstore-node/content/dataloader.html#dataloader - */ - dataloader?: any; - - /** - * By default, gstore-node update en entity in 3 steps (inside a Transaction): fetch the entity in the Datastore, merge the data with the existing entity data and then save the entiy back to the Datastore. If you don't need to merge the data and want to replace whatever is in the Datastore, set `replace: true`. - * - * @type {boolean} - * @default false - */ - replace?: boolean; - } - ): Promise>; - - /** - * Delete an Entity from the Datastore - * - * @static - * @param {(string | number)} id Entity id or name - * @param {(Array)} [ancestors] The entity Ancestors - * @param {string} [namespace] The entity Namespace - * @param {*} [transaction] The current transaction (if any) - * @param {(entity.Key | entity.Key[])} [keys] If you already know the Key, you can provide it instead of passing an id/ancestors/namespace. You might then as well just call "gstore.ds.delete(Key)" but then you would not have the "hooks" triggered in case you have added some in your Schema. - * @returns {Promise<{ success: boolean, key: entity.Key, apiResponse: any }>} - * @link https://sebelga.gitbooks.io/gstore-node/content/model/delete.html - */ - delete( - id?: string | number | (number | string)[], - ancestors?: Array, - namespace?: string, - transaction?: Transaction, - keys?: entity.Key | entity.Key[] - ): Promise; - - /** - * Dynamically remove a property from indexes. If you have set "explicityOnly: false" in your Schema options, then all the properties not declared in the Schema will be included in the indexes. This method allows you to dynamically exclude from indexes certain properties." - * - * @static - * @param {(string | string[])} propName Property name (can be one or an Array of properties) - * @link https://sebelga.gitbooks.io/gstore-node/content/model/other-methods.html - */ - excludeFromIndexes(propName: string | string[]): void; - - /** - * Generates one or several entity key(s) for the Model. - * - * @static - * @param {(string | number)} id Entity id or name - * @param {(Array)} [ancestors] The entity Ancestors - * @param {string} [namespace] The entity Namespace - * @returns {entity.Key} - * @link https://sebelga.gitbooks.io/gstore-node/content/model/key.html - */ - key( - id: string | number, - ancestors?: Array, - namespace?: string - ): entity.Key; - - /** - * Sanitize the data. It will remove all the properties marked as "write: false" and convert "null" (string) to Null. - * - * @param {*} data The data to sanitize - * @returns {*} The data sanitized - * @link https://sebelga.gitbooks.io/gstore-node/content/model/sanitize.html - */ - sanitize(data: { [propName: string]: any }): { [P in keyof T]: T[P] }; - - /** - * Clear all the Queries from the cache *linked* to the Model Entity Kind. You can also pass one or several keys to delete from the cache. - * You normally don't have to call this method as gstore-node does it for you automatically each time you add/edit or delete an entity. - * - * @static - * @param {(entity.Key | entity.Key[])} [keys] Optional entity Keys to remove from the cache with the Queries - * @returns {Promise} - * @link https://sebelga.gitbooks.io/gstore-node/content/model/clearcache.html - */ - clearCache(keys?: entity.Key | entity.Key[]): Promise; - - /** - * Initialize a Datastore Query for the Model's entity Kind - * - * @static - * @param {string} [namespace] Optional namespace for the Query - * @param {Transaction} [transaction] Optional Transaction to run the query into - * @returns {GoogleDatastoreQuery} A Datastore Query instance - * @link https://sebelga.gitbooks.io/gstore-node/content/queries/google-cloud-queries.html - */ - query(namespace?: string, transaction?: Transaction): Query; - - /** - * Shortcut for listing entities from a Model. List queries are meant to quickly list entities with predefined settings without having to create Query instances. - * - * @static - * @param {QueryListOptions} [options] - * @returns {Promise} - * @link https://sebelga.gitbooks.io/gstore-node/content/queries/list.html - */ - list( - options?: U - ): PromiseWithPopulate>; - - /** - * Quickly find an entity by passing key/value pairs. - * - * @static - * @param {{ [propName: string]: any }} keyValues Key / Values pairs - * @param {(Array)} [ancestors] Optional Ancestors to add to the Query - * @param {string} [namespace] Optional Namespace to run the Query into - * @param {({cache?: boolean; ttl?: number | { [propName: string]: number }})} [options] Additional configuration - * @example - ``` - UserModel.findOne({ email: 'john[at]snow.com' }).then(...); - ``` - * @returns {Promise} - * @link https://sebelga.gitbooks.io/gstore-node/content/queries/findone.html - */ - findOne( - keyValues: { [propName: string]: any }, - ancestors?: Array, - namespace?: string, - options?: { - cache?: boolean; - ttl?: number | { [propName: string]: number }; - } - ): PromiseWithPopulate>; - - /** - * Delete all the entities of a Model. It queries the entitites by batches of 500 (maximum set by the Datastore) and delete them. It then repeat the operation until no more entities are found. - * - * @static - * @param {(Array)} [ancestors] Optional Ancestors to add to the Query - * @param {string} [namespace] Optional Namespace to run the Query into - * @returns {Promise<{ success: boolean; message: 'string' }>} - * @link https://sebelga.gitbooks.io/gstore-node/content/queries/deleteall.html - */ - deleteAll( - ancestors?: Array, - namespace?: string - ): Promise<{ success: boolean; message: "string" }>; - - /** - * Find entities before or after an entity based on a property and a value. - * - * @static - * @param {string} propName The property to look around - * @param {*} value The property value - * @param {({ before: number, readAll?:boolean, format?: 'JSON' | 'ENTITY', showKey?: boolean } | { after: number, readAll?:boolean, format?: 'JSON' | 'ENTITY', showKey?: boolean } & QueryOptions)} options Additional configuration - * @returns {Promise} - * @example - ``` - // Find the next 20 post after March 1st 2018 - BlogPost.findAround('publishedOn', '2018-03-01', { after: 20 }) - ``` - * @link https://sebelga.gitbooks.io/gstore-node/content/queries/findaround.html - */ - findAround( - propName: string, - value: any, - options: U - ): PromiseWithPopulate<{ - entities: U["format"] extends EntityFormatType - ? Array> - : Array; - }>; -} - -declare class EntityKlass { - /** - * gstore-node instance - * - * @type {Gstore} - */ - gstore: Gstore; - - /** - * The Entity Model Schema - * - * @type {Schema} - */ - schema: Schema; - - /** - * The Entity Datastore Entity Kind - * - * @type {string} - */ - entityKind: string; - - /** - * The entity KEY - * - * @type {entity.Key} - */ - entityKey: entity.Key; - - /** - * The entity data - * - * @type {*} - */ - entityData: { [P in keyof T]: T[P] }; - - /** - * The entity Key id name or number - * - * @type {(number | string)} - */ - id: number | string; - - /** - * Entity context object to store custom data - * - * @type {*} - */ - context: any; - - /** - * Save the entity in the Datastore - * - * @param {Transaction} [transaction] The current Transaction (if any) - * @param {({ method?: 'upsert' | 'insert' | 'update' })} [options] Additional configuration - * @returns {Promise} - * @link https://sebelga.gitbooks.io/gstore-node/content/entity/methods/save.html - */ - save( - transaction?: Transaction, - options?: { - /** - * The saving method. "upsert" will update the entity *or* create it if it does not exist. "insert" will only save the entity if it *does not* exist. "update" will only save the entity if it *does* exist. - * - * @type {('upsert' | 'insert' | 'update')} - * @default 'upsert' - */ - method?: "upsert" | "insert" | "update"; - } - ): Promise>; - - /** - * Return the entity data object with the entity id. The properties on the Schema where "read" has been set to "false" won't be included unless you set "readAll" to "true" in the options. - * - * @param {{ readAll?: boolean, virtuals?: boolean, showKey?: boolean }} options Additional configuration - * @returns {*} - * @link https://sebelga.gitbooks.io/gstore-node/content/entity/methods/plain.html - */ - plain(options?: { - /** - * Output all the entity data properties, regardless of the Schema "read" setting - * - * @type {boolean} - * @default false - */ - readAll?: boolean; - /** - * Add the virtual properties defined on the Schema - * - * @type {boolean} - * @default false - */ - virtuals?: boolean; - /** - * Add the full entity KEY object in a "__key" property - * - * @type {boolean} - * @default false - */ - showKey?: boolean; - }): T & { [propName: string]: any }; - - /** - * Access a Model from an entity - * - * @param {string} entityKind The entity Kind - * @returns {Model} The Model - * @example - ``` - const user = new User({ name: 'john', pictId: 123}); - const ImageModel = user.model('Image'); - ImageModel.get(user.pictId).then(...); - ``` - * @link https://sebelga.gitbooks.io/gstore-node/content/entity/methods/model.html - */ - model(entityKind: string): Model; - - /** - * Fetch the entity data from the Datastore and merge it in the entity. - * - * @returns {Promise} - */ - datastoreEntity(): Promise>; - - /** - * Validate the entity data. It returns an object with "error" and "value" properties. If the error is null, it is valid. The value returned is the entityData sanitized (unknown properties removed). - * - * @returns {({ error: ValidationError, value: any } | Promise)} - */ - validate(): Validation | Promise; - - /** - * Populate entity references (whose properties are an entity Key) and merge them on the entity - * - * @param refs The entity references to fetch from the Datastore. Can be one or multiple (Array) - * @param properties The properties to return from the reference entities. If not specified, all properties will be returned - */ - populate( - refs?: U, - properties?: U extends Array ? undefined : string | string[] - ): PromiseWithPopulate>; -} - -export const instances: InstancesManager; - -// ----------------------------------------------------------------------- -// -- INTERFACES -// ----------------------------------------------------------------------- - -type EntityFormatType = "ENTITY"; -type JSONFormatType = "JSON"; - -export type Entity = EntityKlass & T; - -type funcReturningPromise = (...args: any[]) => Promise; -interface PromiseWithPopulate extends Promise { - populate: ( - refs?: U, - properties?: U extends Array ? undefined : string | string[] - ) => PromiseWithPopulate; -} - -interface InstancesManager { - set(id: string, instance: Gstore): void - get(id: string): Gstore -} - -export interface QueryResult { - entities: U["format"] extends EntityFormatType - ? Array> - : Array; - nextPageCursor?: string; -} - -export type DeleteResult = { key: entity.Key; success?: boolean; apiResponse?: any }; - -export type PropType = - | "string" - | "int" - | "double" - | "boolean" - | "datetime" - | "array" - | "object" - | "geoPoint" - | "buffer" - | "entityKey" - | NumberConstructor - | StringConstructor - | ObjectConstructor - | ArrayConstructor - | BooleanConstructor - | DateConstructor - | typeof Buffer; - -/** - * gstore-node instance configuration - * - * @interface GstoreConfig - */ -export interface GstoreConfig { - namespace?: string; - cache?: Boolean | CacheConfig; - /** - * If set to "true" (defaut), when fetching an entity by key and the entity is not found in the Datastore, - gstore will throw a "ERR_ENTITY_NOT_FOUND" error. If set to "false", "null" will be returned. - */ - errorOnEntityNotFound?: Boolean; -} - -/** - * Cache configuration - * All the properties are optional. - * - * @interface CacheConfig - */ -export interface CacheConfig { - stores?: Array; - config?: CacheConfigOptions; -} - -export interface CacheConfigOptions { - ttl: { - keys: number; - queries: number; - memory?: { - keys: number; - queries: number; - }; - redis?: { - keys: number; - queries: number; - }; - [key: string]: - | { - keys: number; - queries: number; - } - | number - | void; - }; - cachePrefix?: { - keys: string; - queries: string; - }; - global?: boolean; -} - -export interface SchemaPathDefinition { - type?: PropType; - validate?: - | string - | { rule: string | ((...args: any[]) => boolean); args: any[] }; - optional?: boolean; - default?: any; - excludeFromIndexes?: boolean | string | string[]; - read?: boolean; - excludeFromRead?: string[]; - write?: boolean; - required?: boolean; - joi?: any; - ref?: string; -} - -export interface SchemaOptions { - validateBeforeSave?: boolean; - explicitOnly?: boolean; - excludeLargeProperties?: boolean; - queries?: { - readAll?: boolean; - format?: JSONFormatType | EntityFormatType; - showKey?: string; - }; - joi?: boolean | { extra?: any; options?: any }; -} - -export interface QueryOptions { - /** - * Specify either strong or eventual. If not specified, default values are chosen by Datastore for the operation. Learn more about strong and eventual consistency in the link below - * - * @type {('strong' | 'eventual')} - * @link https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore - */ - consistency?: "strong" | "eventual"; - /** - * If set to true will return all the properties of the entity, regardless of the *read* parameter defined in the Schema - * - * @type {boolean} - * @default false - */ - readAll?: boolean; - /** - * Response format for the entities. Either plain object or entity instances - * - * @type {string} - * @default 'JSON' - */ - format?: JSONFormatType | EntityFormatType; - /** - * Add a "__key" property to the entity data with the complete Key from the Datastore. - * - * @type {boolean} - * @default false - */ - showKey?: boolean; - /** - * If set to true, it will read from the cache and prime the cache with the response of the query. - * - * @type {boolean} - * @default The "global" cache configuration. - */ - cache?: boolean; - /** - * Custom TTL value for the cache. For multi-store it can be an object of ttl values - * - * @type {(number | { [propName: string]: number })} - * @default The cache.ttl.queries value - */ - ttl?: number | { [propName: string]: number }; -} - -export interface QueryListOptions extends QueryOptions { - /** - * Optional namespace for the Query - * - * @type {string} - */ - namespace?: string; - /** - * @type {number} - */ - limit?: number; - /** - * Descending is optional and default to "false" - * - * @example ```{ property: 'userName', descending: true }``` - * @type {({ property: 'string', descending?: boolean } | { property: 'string', descending?: boolean }[])} - */ - order?: - | { property: string; descending?: boolean } - | { property: string; descending?: boolean }[]; - /** - * @type {(string | string[])} - */ - select?: string | string[]; - /** - * @type {([string, any] | [string, string, any] | (any)[][])} - */ - filters?: [string, any] | [string, Operator, any] | (any)[][]; - /** - * @type {Array} - */ - ancestors?: Array; - /** - * @type {string} - */ - start?: string; - /** - * @type {number} - */ - offset?: number; -} - -export interface QueryFindAroundOptions extends QueryOptions { - before?: number; - after?: number; - readAll?: boolean; - format?: JSONFormatType | EntityFormatType; - showKey?: boolean; -} - -export interface Validation { - error: ValidationError; - value: T; -} - -declare class GstoreError extends Error { - name: string; - message: string; - stack: string; - code: keyof ErrorCodes; -} - -declare class ValidationError extends GstoreError {} - -declare class TypeError extends GstoreError {} - -declare class ValueError extends GstoreError {} - -interface ErrorCodes { - ERR_ENTITY_NOT_FOUND: "ERR_ENTITY_NOT_FOUND"; - ERR_GENERIC: "ERR_GENERIC"; - ERR_VALIDATION: "ERR_VALIDATION"; - ERR_PROP_TYPE: "ERR_PROP_TYPE"; - ERR_PROP_VALUE: "ERR_PROP_VALUE"; - ERR_PROP_NOT_ALLOWED: "ERR_PROP_NOT_ALLOWED"; - ERR_PROP_REQUIRED: "ERR_PROP_REQUIRED"; - ERR_PROP_IN_RANGE: "ERR_PROP_IN_RANGE"; -} - -type FunctionPropertiesNames = { - [K in keyof T]: T[K] extends Function ? K : never -}[keyof T]; -type FunctionProperties = Pick>; - -interface GoogleDatastoreQueryMethods { - filter(property: string, operator: Operator, value: any): Query; - filter(property: string, value: any): Query; - - hasAncestor(key: entity.Key): Query; - - order(property: string, options?: { descending: boolean }): Query; - - groupBy(properties: string | ReadonlyArray): Query; - - select(properties: string | ReadonlyArray): Query; - - start(cursorToken: string): Query; - - end(cursorToken: string): Query; - - limit(n: number): Query; - - offset(n: number): Query; - - runStream(): NodeJS.ReadableStream; -} - -export interface Query extends GoogleDatastoreQueryMethods { - run( - options?: U - ): PromiseWithPopulate>; -} - -interface DataLoader {} - -declare namespace GstoreCache { - /** - * gstore-cache Instance - * - * @class Cache - */ - class Cache { - keys: { - read( - keys: entity.Key | entity.Key[], - options?: { ttl: number | { [propName: string]: number } }, - fetchHandler?: (keys: entity.Key | entity.Key[]) => Promise - ): Promise; - get(key: entity.Key): Promise; - mget(...keys: entity.Key[]): Promise; - set( - key: entity.Key, - data: any, - options?: { ttl: number | { [propName: string]: number } } - ): Promise; - mset(...args: any[]): Promise; - del(...keys: entity.Key[]): Promise; - }; - - queries: { - read( - query: GoogleDatastoreQuery, - options?: { ttl: number | { [propName: string]: number } }, - fetchHandler?: (query: GoogleDatastoreQuery) => Promise - ): Promise; - get(query: GoogleDatastoreQuery): Promise; - mget(...queries: GoogleDatastoreQuery[]): Promise; - set( - query: GoogleDatastoreQuery, - data: any, - options?: { ttl: number | { [propName: string]: number } } - ): Promise; - mset(...args: any[]): Promise; - kset( - key: string, - data: any, - entityKinds: string | string[], - options?: { ttl: number } - ): Promise; - clearQueriesByKind(entityKinds: string | string[]): Promise; - del(...queries: GoogleDatastoreQuery[]): Promise; - }; - - /** - * Retrieve an element from the cache - * - * @param {string} key The cache key - */ - get(key: string): Promise; - - /** - * Retrieve multiple elements from the cache - * - * @param {...string[]} keys Unlimited number of keys - */ - mget(...keys: string[]): Promise; - - /** - * Add an element to the cache - * - * @param {string} key The cache key - * @param {*} value The data to save in the cache - */ - set(key: string, value: any): Promise; - - /** - * Add multiple elements into the cache - * - * @param {...any[]} args Key Value pairs (key1, data1, key2, data2...) - */ - mset(...args: any[]): Promise; - - /** - * Remove one or multiple elements from the cache - * - * @param {string[]} keys The keys to remove - */ - del(keys: string[]): Promise; - - /** - * Clear the cache - */ - reset(): Promise; - } -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 18d0c77..0000000 --- a/lib/index.js +++ /dev/null @@ -1,202 +0,0 @@ -'use strict'; - -/* eslint-disable prefer-template */ - -const is = require('is'); -const extend = require('extend'); -const hooks = require('promised-hooks'); -const NsqlCache = require('nsql-cache'); -const dsAdapter = require('nsql-cache-datastore'); - -const Schema = require('./schema'); -const Model = require('./model'); -const { queries } = require('./constants'); -const { - GstoreError, - ValidationError, - TypeError, - ValueError, - errorCodes, -} = require('./errors'); -const defaultValues = require('./helpers/defaultValues'); -const datastoreSerializer = require('./serializer').Datastore; -const { createDataLoader } = require('./dataloader'); - -const pkg = require('../package.json'); - -const defaultConfig = { - cache: undefined, - errorOnEntityNotFound: true, -}; - -class Gstore { - constructor(config = {}) { - if (!is.object(config)) { - throw new Error('Gstore config must be an object.'); - } - - this.models = {}; - this.modelSchemas = {}; - this.options = {}; - this.config = { ...defaultConfig, ...config }; - this.Schema = Schema; - this.Queries = queries; - this._defaultValues = defaultValues; - this._pkgVersion = pkg.version; - - this.errors = { - GstoreError, - ValidationError, - TypeError, - ValueError, - codes: errorCodes, - }; - this.ERR_HOOKS = hooks.ERRORS; - this.createDataLoader = createDataLoader; - } - - model(entityKind, schema, options) { - if (is.object(schema) && !(schema.instanceOfSchema)) { - schema = new Schema(schema); - } - - options = options || {}; - - // look up schema in cache - if (!this.modelSchemas[entityKind]) { - if (schema) { - // cache it so we only apply plugins once - this.modelSchemas[entityKind] = schema; - } else { - throw new Error(`Schema ${entityKind} missing`); - } - } - - // we might be passing a different schema for - // an existing model entityKind. in this case don't read from cache. - if (this.models[entityKind] && options.cache !== false) { - if (schema && schema.instanceOfSchema && schema !== this.models[entityKind].schema) { - throw new Error('Trying to override ' + entityKind + ' Model Schema'); - } - return this.models[entityKind]; - } - - const model = Model.compile(entityKind, schema, this); - - if (options.cache === false) { - return model; - } - - this.models[entityKind] = model; - - return this.models[entityKind]; - } - - /** - * Alias to gcloud datastore Transaction method - */ - transaction() { - return this._ds.transaction(); - } - - /** - * Return an array of model names created on this instance of Gstore - * @returns {Array} - */ - modelNames() { - const names = Object.keys(this.models); - return names; - } - - save(entities, transaction, options = {}) { - if (!entities) { - throw new Error('No entities passed'); - } - - /** - * Validate entities before saving - */ - if (options.validate) { - let error; - const validateEntity = entity => { - ({ error } = entity.validate()); - if (error) { - throw error; - } - }; - try { - if (Array.isArray(entities)) { - entities.forEach(validateEntity); - } else { - validateEntity(entities); - } - } catch (err) { - return Promise.reject(err); - } - } - - // Convert gstore entities to datastore forma ({key, data}) - const entitiesSerialized = datastoreSerializer.entitiesToDatastore(entities, options); - - if (transaction) { - return transaction.save(entitiesSerialized); - } - - // We forward the call to google-datastore - return this._ds.save.call(this._ds, entitiesSerialized); - } - - // Connect to Google Datastore instance - connect(ds) { - if (!ds.constructor || ds.constructor.name !== 'Datastore') { - throw new Error('No @google-cloud/datastore instance provided.'); - } - - this._ds = ds; - - if (this.config.cache) { - const defaultCacheSettings = { - config: { - wrapClient: false, - }, - }; - const cacheSettings = this.config.cache === true - ? defaultCacheSettings - : extend(true, {}, defaultCacheSettings, this.config.cache); - const { stores, config } = cacheSettings; - const db = dsAdapter(ds); - this.cache = new NsqlCache({ db, stores, config }); - delete this.config.cache; - } - } - - /** - * Expose the defaultValues constants - */ - get defaultValues() { - return this._defaultValues; - } - - get version() { - return this._pkgVersion; - } - - get ds() { - return this._ds; - } -} - -const instances = { - refs: new Map(), - get(id) { - return this.refs.get(id); - }, - set(id, instance) { - this.refs.set(id, instance); - }, -}; - -module.exports = { - Gstore, - instances, -}; diff --git a/lib/model.js b/lib/model.js deleted file mode 100755 index 60ebf16..0000000 --- a/lib/model.js +++ /dev/null @@ -1,967 +0,0 @@ -/* eslint-disable max-classes-per-file */ - -'use strict'; - -const is = require('is'); -const arrify = require('arrify'); -const extend = require('extend'); -const hooks = require('promised-hooks'); -const dsAdapter = require('nsql-cache-datastore')(); -const get = require('lodash.get'); -const set = require('lodash.set'); - -const Entity = require('./entity'); -const Query = require('./query'); -const { GstoreError, errorCodes } = require('./errors'); -const { populateHelpers } = require('./helpers'); - -const { keyToString } = dsAdapter; -const { populateFactory } = populateHelpers; - -class Model extends Entity { - static compile(kind, schema, gstore) { - const ModelInstance = class extends Model { }; - - // Wrap the Model to add "pre" and "post" hooks functionalities - hooks.wrap(ModelInstance); - - ModelInstance.schema = schema; - ModelInstance.schema.__meta = metaData(); - ModelInstance.registerHooksFromSchema(); - - /** - * Add schema "custom" methods on the prototype - * to be accesible from Entity instances - */ - applyMethods(ModelInstance.prototype, schema); - applyStatics(ModelInstance, schema); - - ModelInstance.prototype.entityKind = kind; - ModelInstance.entityKind = kind; - - ModelInstance.prototype.gstore = gstore; - ModelInstance.gstore = gstore; - - /** - * Create virtual properties (getters and setters for "virtuals" defined on the Schema) - */ - Object.keys(schema.virtuals) - .filter(key => ({}.hasOwnProperty.call(schema.virtuals, key))) - .forEach(key => Object.defineProperty(ModelInstance.prototype, key, { - get: function getProp() { - return schema.virtuals[key].applyGetters({ ...this.entityData }); - }, - set: function setProp(newValue) { - schema.virtuals[key].applySetters(newValue, this.entityData); - }, - })); - - return ModelInstance; - - // --------------- - - // To improve performance and avoid looping over and over the entityData or Schema - // we keep here some meta data to be used later in models and entities methods - function metaData() { - const meta = {}; - - Object.keys(schema.paths).forEach(k => { - switch (schema.paths[k].type) { - case 'geoPoint': - // This allows us to automatically convert valid lng/lat objects - // to Datastore.geoPoints - meta.geoPointsProps = meta.geoPointsProps || []; - meta.geoPointsProps.push(k); - break; - case 'entityKey': - meta.refProps = meta.refProps || {}; - meta.refProps[k] = true; - break; - default: - } - }); - - return meta; - } - } - - /** - * Pass all the "pre" and "post" hooks from schema to - * the current ModelInstance - */ - static registerHooksFromSchema() { - const self = this; - const callQueue = this.schema.callQueue.model; - - if (!Object.keys(callQueue).length) { - return this; - } - - Object.keys(callQueue).forEach(addHooks); - - return self; - - // -------------------------------------- - - function addHooks(method) { - // Add Pre hooks - callQueue[method].pres.forEach(fn => { - self.pre(method, fn); - }); - - // Add Post hooks - callQueue[method].post.forEach(fn => { - self.post(method, fn); - }); - } - } - - /** - * Get and entity from the Datastore - */ - static get(id, ancestors, namespace, transaction, options = {}) { - const ids = arrify(id); - const _this = this; - - const key = this.key(ids, ancestors, namespace); - const refsToPopulate = []; - const { dataloader } = options; - - /** - * If gstore has been initialize with a cache we first fetch - * the key(s) from it. - * gstore-cache underneath will call the "fetchHandler" with only the keys that haven't - * been found. The final response is the merge of the cache result + the fetch. - */ - const promise = this.fetchEntityByKey(key, transaction, dataloader, options) - .then(onEntity) - .then(this.populate(refsToPopulate, { ...options, transaction })); - - promise.populate = populateFactory(refsToPopulate, promise, this); - return promise; - - // ---------- - - function onEntity(entityDataFetched) { - const entityData = arrify(entityDataFetched); - - if (ids.length === 1 - && (entityData.length === 0 || typeof entityData[0] === 'undefined' || entityData[0] === null)) { - if (_this.gstore.config.errorOnEntityNotFound) { - return Promise.reject(new GstoreError( - errorCodes.ERR_ENTITY_NOT_FOUND, - `${_this.entityKind} { ${ids[0].toString()} } not found` - )); - } - - return null; - } - - const entity = entityData - .map(data => { - if (typeof data === 'undefined' || data === null) { - return null; - } - return _this.__model(data, null, null, null, data[_this.gstore.ds.KEY]); - }); - - if (Array.isArray(id) - && options.preserveOrder - && entity.every(e => typeof e !== 'undefined' && e !== null)) { - entity.sort((a, b) => id.indexOf(a.entityKey.id) - id.indexOf(b.entityKey.id)); - } - - return Array.isArray(id) ? entity : entity[0]; - } - } - - static fetchEntityByKey(key, transaction, dataloader, options) { - const handler = _keys => { - const keys = arrify(_keys); - if (transaction) { - if (transaction.constructor.name !== 'Transaction') { - return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); - } - return transaction.get(keys).then(([result]) => arrify(result)); - } - - if (dataloader) { - if (dataloader.constructor.name !== 'DataLoader') { - return Promise.reject( - new GstoreError(errorCodes.ERR_GENERIC, 'dataloader must be a "DataLoader" instance') - ); - } - return dataloader.loadMany(keys).then(result => arrify(result)); - } - return this.gstore.ds.get(keys).then(([result]) => arrify(result)); - }; - - if (this.__hasCache(options)) { - return this.gstore.cache.keys.read( - // nsql-cache requires an array for multiple and a single key when *not* multiple - Array.isArray(key) && key.length === 1 ? key[0] : key, options, handler - ); - } - return handler(key); - } - - static update(id, data, ancestors, namespace, transaction, options) { - this.__hooksEnabled = true; - const _this = this; - - let entityUpdated; - - const key = this.key(id, ancestors, namespace); - const replace = options && options.replace === true; - - let internalTransaction = false; - - /** - * If options.replace is set to true we don't fetch the entity - * and save the data directly to the specified key, overriding any previous data. - */ - if (replace) { - return saveEntity({ key, data }) - .then(onEntityUpdated) - .catch(onUpdateError); - } - - if (typeof transaction === 'undefined' || transaction === null) { - internalTransaction = true; - transaction = this.gstore.ds.transaction(); - return transaction - .run() - .then(getAndUpdate) - .catch(onUpdateError); - } - - if (transaction.constructor.name !== 'Transaction') { - return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); - } - - return getAndUpdate(); - - // --------------------------------------------------------- - - function getAndUpdate() { - return getEntity() - .then(saveEntity) - .then(onEntityUpdated); - } - - function getEntity() { - return transaction - .get(key) - .then(getData => { - const entity = getData[0]; - - if (typeof entity === 'undefined') { - throw (new GstoreError( - errorCodes.ERR_ENTITY_NOT_FOUND, - `Entity { ${id.toString()} } to update not found` - )); - } - - extend(false, entity, data); - - const result = { - key: entity[_this.gstore.ds.KEY], - data: entity, - }; - - return result; - }); - } - - function saveEntity(getData) { - const entityKey = getData.key; - const entityData = getData.data; - const model = _this.__model(entityData, null, null, null, entityKey); - - /** - * If a DataLoader instance is passed in the options - * attach it to the entity so it is available in "pre" hooks - */ - if (options && options.dataloader) { - model.dataloader = options.dataloader; - } - - return model.save(transaction); - } - - function onEntityUpdated(entity) { - entityUpdated = entity; - - if (options && options.dataloader) { - options.dataloader.clear(key); - } - - if (internalTransaction) { - // If we created the Transaction instance internally for the update, we commit it - // otherwise we leave the commit() call to the transaction creator - return transaction - .commit() - .then(() => transaction.execPostHooks().catch(err => { - entityUpdated[ - entityUpdated.gstore.ERR_HOOKS - ] = (entityUpdated[entityUpdated.gstore.ERR_HOOKS] || []).push(err); - })) - .then(onTransactionSuccess); - } - - return onTransactionSuccess(); - } - - function onUpdateError(err) { - const error = Array.isArray(err) ? err[0] : err; - if (internalTransaction) { - // If we created the Transaction instance internally for the update, we rollback it - // otherwise we leave the rollback() call to the transaction creator - return transaction.rollback().then(() => { - throw error; - }); - } - - throw error; - } - - function onTransactionSuccess() { - /** - * Make sure to delete the cache for this key - */ - if (_this.__hasCache(options)) { - return _this.clearCache(key) - .then(() => entityUpdated) - .catch(err => { - let msg = 'Error while clearing the cache after updating the entity.'; - msg += 'The entity has been updated successfully though. '; - msg += 'Both the cache error and the entity updated have been attached.'; - const cacheError = new Error(msg); - cacheError.__entityUpdated = entityUpdated; - cacheError.__cacheError = err; - throw cacheError; - }); - } - - return entityUpdated; - } - } - - static delete(id, ancestors, namespace, transaction, key, options = {}) { - const _this = this; - this.__hooksEnabled = true; - - if (!key) { - key = this.key(id, ancestors, namespace); - } - - if (transaction && transaction.constructor.name !== 'Transaction') { - return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); - } - - /** - * If it is a transaction, we create a hooks.post array that will be executed - * when transaction succeeds by calling transaction.execPostHooks() ---> returns a Promise - */ - if (transaction) { - // disable (post) hooks, to only trigger them if transaction succeeds - this.__hooksEnabled = false; - this.hooksTransaction(transaction, this.__posts ? this.__posts.delete : undefined); - transaction.delete(key); - return Promise.resolve(); - } - - return this.gstore.ds.delete(key).then(onDelete); - - // ------------------------------------------------------- - - function onDelete(results) { - const response = results ? results[0] : {}; - response.key = key; - - /** - * If we passed a DataLoader instance, we clear its cache - */ - if (options.dataloader) { - options.dataloader.clear(key); - } - - if (typeof response.indexUpdates !== 'undefined') { - response.success = response.indexUpdates > 0; - } - - /** - * Make sure to delete the cache for this key - */ - if (_this.__hasCache(options)) { - return _this.clearCache(key, options.clearQueries) - .then(() => response) - .catch(err => { - let msg = 'Error while clearing the cache after deleting the entity.'; - msg += 'The entity has been deleted successfully though. '; - msg += 'The cache error has been attached.'; - const cacheError = new Error(msg); - cacheError.__response = response; - cacheError.__cacheError = err; - throw cacheError; - }); - } - - return response; - } - } - - static deleteAll(ancestors, namespace) { - const _this = this; - - const maxEntitiesPerBatch = 500; - const timeoutBetweenBatches = 500; - - /** - * We limit the number of entities fetched to 100.000 to avoid hang up the system when - * there are > 1 million of entities to delete - */ - const limitDataPerQuery = 100000; - - let currentBatch; - let entities; - let totalBatches; - - return createQueryWithLimit().run({ cache: false }).then(onEntities); - - // ------------------------------------------------ - - function createQueryWithLimit() { - // We query only limit number in case of big table - // If we query with more than million data query will hang up - const query = _this.initQuery(namespace); - if (ancestors) { - query.hasAncestor(_this.gstore.ds.key(ancestors.slice())); - } - query.select('__key__'); - query.limit(limitDataPerQuery); - - return query; - } - - function onEntities(data) { - // [entities] = data; - ({ entities } = data); - - if (entities.length === 0) { - // No more Data in table - return { - success: true, - message: `All ${_this.entityKind} deleted successfully.`, - }; - } - - currentBatch = 0; - - // We calculate the total batches we will need to process - // The Datastore does not allow more than 500 keys at once when deleting. - totalBatches = Math.ceil(entities.length / maxEntitiesPerBatch); - - return deleteEntities(currentBatch); - } - - function deleteEntities(batch) { - const indexStart = batch * maxEntitiesPerBatch; - const indexEnd = indexStart + maxEntitiesPerBatch; - const entitiesToDelete = entities.slice(indexStart, indexEnd); - - if ((_this.__pres && {}.hasOwnProperty.call(_this.__pres, 'delete'))) { - // We execute delete in serie (chaining Promises) --> so we call each possible pre & post hooks - return entitiesToDelete.reduce(chainPromise, Promise.resolve()) - .then(onEntitiesDeleted); - } - - const keys = entitiesToDelete.map(entity => entity[_this.gstore.ds.KEY]); - - // We only need to clear the Queries from the cache once, - // so we do it on the first batch. - const clearQueries = currentBatch === 0; - return _this.delete.call(_this, null, null, null, null, keys, { clearQueries }) - .then(onEntitiesDeleted); - } - - function onEntitiesDeleted() { - currentBatch += 1; - - if (currentBatch < totalBatches) { - // Still more batches to process - return new Promise(resolve => { - setTimeout(resolve, timeoutBetweenBatches); - }).then(() => deleteEntities(currentBatch)); - } - - // Re-run the fetch Query in case there are still entities to delete - return createQueryWithLimit().run().then(onEntities); - } - - function chainPromise(promise, entity) { - return promise.then(() => _this.delete.call(_this, null, null, null, null, entity[_this.gstore.ds.KEY])); - } - } - - /** - * Generate one or an Array of Google Datastore entity keys - * based on the current entity kind - * - * @param {Number|String|Array} ids Id of the entity(ies) - * @param {Array} ancestors Ancestors path (otional) - * @namespace {String} namespace The namespace where to store the entity - */ - static key(ids, ancestors, namespace) { - const _this = this; - const keys = []; - - let multiple = false; - - if (typeof ids !== 'undefined' && ids !== null) { - ids = arrify(ids); - - multiple = ids.length > 1; - - ids.forEach(id => { - const key = getKey(id); - keys.push(key); - }); - } else { - const key = getKey(null); - keys.push(key); - } - - return multiple ? keys : keys[0]; - - // ---------------------------------------- - - function getKey(id) { - const path = getPath(id); - let key; - - if (typeof namespace !== 'undefined' && namespace !== null) { - key = _this.gstore.ds.key({ - namespace, - path, - }); - } else { - key = _this.gstore.ds.key(path); - } - return key; - } - - function getPath(id) { - let path = [_this.entityKind]; - - if (typeof id !== 'undefined' && id !== null) { - path.push(id); - } - - if (ancestors && is.array(ancestors)) { - path = ancestors.concat(path); - } - - return path; - } - } - - /** - * Add "post" hooks to a transaction - */ - static hooksTransaction(transaction, postHooks) { - const _this = this; - postHooks = arrify(postHooks); - - if (!{}.hasOwnProperty.call(transaction, 'hooks')) { - transaction.hooks = { - post: [], - }; - } - - postHooks.forEach(hook => transaction.hooks.post.push(hook)); - - transaction.execPostHooks = function executePostHooks() { - if (this.hooks.post) { - return this.hooks.post.reduce((promise, hook) => promise.then(hook.bind(_this)), Promise.resolve()); - } - - return Promise.resolve(); - }; - } - - /** - * Dynamic properties (in non explicitOnly Schemas) are indexes by default - * This method allows to exclude from indexes those properties if needed - * @param properties {Array} or {String} - * @param cb - */ - static excludeFromIndexes(properties) { - properties = arrify(properties); - - properties.forEach(prop => { - if (!{}.hasOwnProperty.call(this.schema.paths, prop)) { - this.schema.path(prop, { optional: true, excludeFromIndexes: true }); - } else { - this.schema.paths[prop].excludeFromIndexes = true; - } - }); - } - - /** - * Sanitize user data before saving to Datastore - * @param data : userData - */ - static sanitize(data, options = { disabled: [] }) { - const { schema } = this; - const key = data[this.gstore.ds.KEY]; // save the Key - - if (!is.object(data)) { - return null; - } - - const isJoiSchema = schema.isJoi; - - let sanitized; - let joiOptions; - if (isJoiSchema) { - const { error, value } = schema.validateJoi(data); - if (!error) { - sanitized = { ...value }; - } - joiOptions = schema.options.joi.options || {}; - } - if (sanitized === undefined) { - sanitized = { ...data }; - } - - const isSchemaExplicitOnly = isJoiSchema - ? joiOptions.stripUnknown - : schema.options.explicitOnly === true; - - const isWriteDisabled = options.disabled.includes('write'); - const hasSchemaRefProps = Boolean(schema.__meta.refProps); - let schemaHasProperty; - let isPropWritable; - let propValue; - - Object.keys(data).forEach(k => { - schemaHasProperty = {}.hasOwnProperty.call(schema.paths, k); - isPropWritable = schemaHasProperty - ? schema.paths[k].write !== false - : true; - propValue = sanitized[k]; - - if ((isSchemaExplicitOnly && !schemaHasProperty) || (!isPropWritable && !isWriteDisabled)) { - delete sanitized[k]; - } else if (propValue === 'null') { - sanitized[k] = null; - } else if (hasSchemaRefProps && schema.__meta.refProps[k] && !this.gstore.ds.isKey(propValue)) { - // Replace populated entity by their entity Key - if (is.object(propValue) && propValue[this.gstore.ds.KEY]) { - sanitized[k] = propValue[this.gstore.ds.KEY]; - } - } - }); - - return key ? { ...sanitized, [this.gstore.ds.KEY]: key } : sanitized; - } - - /** - * Clears all the cache related to the Model Entity Kind - * If keys are passed, it will delete those keys, otherwise it will delete - * all the queries in the cache linked to the Model Entity kind. - * @param {DatastoreKeys} keys Keys to delete from the cache - */ - static clearCache(_keys, clearQueries = true) { - const handlers = []; - - if (clearQueries) { - handlers.push(this.gstore.cache.queries.clearQueriesByKind(this.entityKind) - .catch(e => { - if (e.code === 'ERR_NO_REDIS') { - // Silently fail if no Redis Client - return; - } - throw e; - })); - } - - if (_keys) { - const keys = arrify(_keys); - handlers.push(this.gstore.cache.keys.del(...keys)); - } - - return Promise.all(handlers).then(() => ({ success: true })); - } - - static populate(refs, options = {}) { - const _this = this; - const dataloader = options.dataloader || this.gstore.createDataLoader(); - - const getPopulateMetaForEntity = (entity, entityRefs) => { - const keysToFetch = []; - const mapKeyToPropAndSelect = {}; - - const isEntityClass = entity instanceof Model; - entityRefs.forEach(ref => { - const { path } = ref; - const entityData = isEntityClass ? entity.entityData : entity; - - const key = get(entityData, path); - if (!key) { - set(entityData, path, null); - return; - } - - if (!this.gstore.ds.isKey(key)) { - throw new Error(`[gstore] ${path} is not a Datastore Key. Reference entity can't be fetched.`); - } - - // Stringify the key - const strKey = keyToString(key); - // Add it to our map - mapKeyToPropAndSelect[strKey] = { ref }; - // Add to our array to be fetched - keysToFetch.push(key); - }); - - return { entity, keysToFetch, mapKeyToPropAndSelect }; - }; - - return entitiesToProcess => { - if (!refs || !refs.length) { - // Nothing to do here... - return Promise.resolve(entitiesToProcess); - } - - // Keep track if we provided an array for the response format - const isArray = Array.isArray(entitiesToProcess); - const entities = arrify(entitiesToProcess); - const isEntityClass = entities[0] instanceof Model; - - // Fetches the entity references at the current - // object tree depth - const fetchRefsEntitiesRefsAtLevel = entityRefs => { - // For each one of the entities to process, we gatter some meta data - // like the keys to fetch for that entity in order to populate its refs. - // Dataloaader will take care to only fetch unique keys on the Datastore - const meta = entities.map(entity => getPopulateMetaForEntity(entity, entityRefs)); - - const onKeysFetched = (response, { entity, keysToFetch, mapKeyToPropAndSelect }) => { - if (!response) { - // No keys have been fetched - return; - } - - const entityData = isEntityClass ? { ...entity.entityData } : entity; - - const mergeRefEntitiesToEntityData = (data, i) => { - const key = keysToFetch[i]; - const strKey = keyToString(key); - const { ref: { path, select } } = mapKeyToPropAndSelect[strKey]; - - if (!data) { - set(entityData, path, data); - return; - } - - const EmbeddedModel = _this.gstore.model(key.kind); - const embeddedEntity = new EmbeddedModel(data, null, null, null, key); - - // If "select" fields are provided, we return them, - // otherwise we return the entity plain() json - const json = select.length && !select.some(s => s === '*') - ? select.reduce((acc, field) => ({ - ...acc, - [field]: data[field] || null, - }), {}) - : embeddedEntity.plain(); - - set(entityData, path, { ...json, id: key.name || key.id }); - - if (isEntityClass) { - entity.entityData = entityData; - } - }; - - // Loop over all dataloader.loadMany() responses - response.forEach(mergeRefEntitiesToEntityData); - }; - - const promises = meta.map(({ keysToFetch }) => (keysToFetch.length - ? this.fetchEntityByKey(keysToFetch, options.transaction, dataloader, options) - : Promise.resolve(null))); - - return Promise.all(promises).then(result => { - // Loop over all responses from dataloader.loadMany() calls - result.forEach((res, i) => onKeysFetched(res, meta[i])); - }); - }; - - return new Promise((resolve, reject) => { - // At each tree level we fetch the entity references in series. - refs.reduce((chainedPromise, entityRefs) => chainedPromise.then(() => ( - fetchRefsEntitiesRefsAtLevel(entityRefs) - )), Promise.resolve()) - .then(() => { - resolve(isArray ? entities : entities[0]); - }) - .catch(reject); - }); - }; - } - - /** - * Returns all the schema properties that are references - * to other entities (their value is an entity Key) - */ - static getEntitiesRefsFromSchema() { - return Object.entries(this.schema.paths) - .filter(({ 1: pathConfig }) => pathConfig.type === 'entityKey') - .map(({ 0: ref }) => ref); - } - - // ------------------------------------------------------------------------ - // "Private" methods - // ------------------------------------------------------------------------ - - /** - * Creates an entity instance of a Model - * @param data (entity data) - * @param id - * @param ancestors - * @param namespace - * @param key (gcloud entity Key) - * @returns {Entity} Entity --> Model instance - * @private - */ - static __model(data, id, ancestors, namespace, key) { - const M = this.compile(this.entityKind, this.schema, this.gstore); - return new M(data, id, ancestors, namespace, key); - } - - /** - * Helper to change the function scope for a hook if necessary - * - * @param {String} hook The name of the hook (save, delete...) - * @param {Array} args The arguments passed to the original method - */ - static __scopeHook(hook, args, hookName, hookType) { - const _this = this; - - switch (hook) { - case 'delete': - return getScopeForDeleteHooks(); - default: - return _this; - } - - /** - * For "delete" hooks we want to set the scope to - * the entity instance we are going to delete - * We won't have any entity data inside the entity but, if needed, - * we can then call the "datastoreEntity()" helper on the scope (this) - * from inside the hook. - * For "multiple" ids to delete, we obviously can't set any scope. - */ - function getScopeForDeleteHooks() { - const id = is.object(args[0]) && {}.hasOwnProperty.call(args[0], '__override') - ? arrify(args[0].__override)[0] - : args[0]; - - if (is.array(id)) { - return null; - } - - let ancestors; - let namespace; - let key; - - if (hookType === 'post') { - ({ key } = args); - if (is.array(key)) { - return null; - } - } else { - ({ - 1: ancestors, - 2: namespace, - 4: key, - } = args); - } - - if (!id && !ancestors && !namespace && !key) { - return undefined; - } - - return _this.__model(null, id, ancestors, namespace, key); - } - } - - /** - * Helper to know if the cache is "on" to fetch entities or run a query - * - * @static - * @private - * @param {any} options The query options object - * @param {string} [type='keys'] The type of fetching. Can either be 'keys' or 'queries' - * @returns {boolean} - * @memberof Model - */ - static __hasCache(options = {}, type = 'keys') { - if (typeof this.gstore.cache === 'undefined') { - return false; - } - if (typeof options.cache !== 'undefined') { - return options.cache; - } - if (this.gstore.cache.config.global === false) { - return false; - } - if (this.gstore.cache.config.ttl[type] === -1) { - return false; - } - return true; - } -} - -/** - * Add custom methods declared on the Schema to the Entity Class - * - * @param {Entity} entity Model.prototype - * @param {any} schema Model Schema - * @returns Model.prototype - */ -function applyMethods(entity, schema) { - Object.keys(schema.methods).forEach(method => { - entity[method] = schema.methods[method]; - }); - return entity; -} - -function applyStatics(_Model, schema) { - Object.keys(schema.statics).forEach(method => { - if (typeof _Model[method] !== 'undefined') { - throw new Error(`${method} already declared as static.`); - } - _Model[method] = schema.statics[method]; - }); - return _Model; -} - -// Bind Query methods -const { - initQuery, - list, - findOne, - findAround, -} = new Query(); - -Model.initQuery = initQuery; -Model.query = initQuery; // create alias -Model.list = list; -Model.findOne = findOne; -Model.findAround = findAround; - -module.exports = Model; diff --git a/lib/query.js b/lib/query.js deleted file mode 100644 index a538c37..0000000 --- a/lib/query.js +++ /dev/null @@ -1,214 +0,0 @@ -'use strict'; - -const extend = require('extend'); -const is = require('is'); - -const { queryHelpers, populateHelpers } = require('./helpers'); -const { GstoreError, errorCodes } = require('./errors'); -const datastoreSerializer = require('./serializer').Datastore; - -const { populateFactory } = populateHelpers; - -class Query { - constructor(Model) { - this.Model = Model; - } - - /** - * Initialize a query on the Model Entity Kind - * - * @param {String} namespace Namespace for the Query - * @param {Object} transaction The transactioh to execute the query in (optional) - * - * @returns {Object} The query to be run - */ - initQuery(namespace, transaction) { - const Model = this.Model || this; - const query = createDatastoreQuery(Model, namespace, transaction); - - // keep a reference to original run() method - query.__originalRun = query.run; - - query.run = function runQuery(options = {}, responseHandler = res => res) { - options = extend(true, {}, Model.schema.options.queries, options); - - /** - * Array to keep all the references entities to fetch - */ - const refsToPopulate = []; - let promise; - - const populateHandler = response => (refsToPopulate.length - ? Model.populate(refsToPopulate, options)(response.entities) - .then(entities => ({ ...response, entities })) - : response); - - if (Model.__hasCache(options, 'queries')) { - promise = Model.gstore.cache.queries.read(query, options, query.__originalRun.bind(query)) - .then(onQuery) - .then(populateHandler) - .then(responseHandler); - } else { - promise = this.__originalRun.call(query, options) - .then(onQuery) - .then(populateHandler) - .then(responseHandler); - } - - promise.populate = populateFactory(refsToPopulate, promise, Model); - return promise; - - // ----------------------------------------------- - - function onQuery(data) { - let entities = data[0]; - const info = data[1]; - - // Add id property to entities and suppress properties - // where "read" setting is set to false - entities = entities.map(entity => ( - datastoreSerializer.fromDatastore.call(Model, entity, options) - )); - - const response = { - entities, - }; - - if (info.moreResults !== Model.gstore.ds.NO_MORE_RESULTS) { - response.nextPageCursor = info.endCursor; - } - - return response; - } - }; - - return query; - } - - list(options = {}) { - const Model = this.Model || this; - - /** - * If global options set in schema, we extend it with passed options - */ - if ({}.hasOwnProperty.call(Model.schema.shortcutQueries, 'list')) { - options = extend({}, Model.schema.shortcutQueries.list, options); - } - - /** - * Query.initQuery() has been binded to Model.query() method - */ - let query = Model.query(options.namespace); - - /** - * Build Datastore Query from options passed - */ - query = queryHelpers.buildFromOptions(query, options, Model.gstore.ds); - - const { - limit, offset, order, select, ancestors, filters, start, ...rest - } = options; - return query.run(rest); - } - - findOne(params, ancestors, namespace, options) { - const Model = this.Model || this; - Model.__hooksEnabled = true; - - if (!is.object(params)) { - return Promise.reject(new Error('[gstore.findOne()]: "Params" has to be an object.')); - } - - const query = Model.query(namespace); - query.limit(1); - - Object.keys(params).forEach(k => { - query.filter(k, params[k]); - }); - - if (ancestors) { - query.hasAncestor(Model.gstore.ds.key(ancestors.slice())); - } - - const responseHandler = ({ entities }) => { - if (entities.length === 0) { - if (Model.gstore.config.errorOnEntityNotFound) { - throw new GstoreError( - errorCodes.ERR_ENTITY_NOT_FOUND, - `${Model.entityKind} not found` - ); - } - return null; - } - - const [e] = entities; - const entity = Model.__model(e, null, null, null, e[Model.gstore.ds.KEY]); - return entity; - }; - return query.run(options, responseHandler); - } - - findAround(property, value, options, namespace) { - const Model = this.Model || this; - - const { error } = validateArguments(); - if (error) { - return Promise.reject(error); - } - - const query = Model.query(namespace); - const op = options.after ? '>' : '<'; - const descending = !!options.after; - - query.filter(property, op, value); - query.order(property, { descending }); - query.limit(options.after ? options.after : options.before); - - const { after, before, ...rest } = options; - return query.run(rest, ({ entities }) => entities); - - // ----------- - - function validateArguments() { - if (!property || !value || !options) { - return { error: new Error('[gstore.findAround()]: Not all the arguments were provided.') }; - } - - if (!is.object(options)) { - return { error: new Error('[gstore.findAround()]: Options pased has to be an object.') }; - } - - if (!{}.hasOwnProperty.call(options, 'after') && !{}.hasOwnProperty.call(options, 'before')) { - return { error: new Error('[gstore.findAround()]: You must set "after" or "before" in options.') }; - } - - if ({}.hasOwnProperty.call(options, 'after') && {}.hasOwnProperty.call(options, 'before')) { - return { error: new Error('[gstore.findAround()]: You can\'t set both "after" and "before".') }; - } - - return { error: null }; - } - } -} - -// ---------- - -function createDatastoreQuery(Model, namespace, transaction) { - if (transaction && transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } - - const createQueryArgs = [Model.entityKind]; - - if (namespace) { - createQueryArgs.unshift(namespace); - } - - if (transaction) { - return transaction.createQuery.apply(transaction, createQueryArgs); - } - - return Model.gstore.ds.createQuery.apply(Model.gstore.ds, createQueryArgs); -} - -module.exports = Query; diff --git a/lib/schema.js b/lib/schema.js deleted file mode 100644 index ed8e2b2..0000000 --- a/lib/schema.js +++ /dev/null @@ -1,259 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -'use strict'; - -const optional = require('optional'); -const extend = require('extend'); -const is = require('is'); - -const Joi = optional('@hapi/joi') || optional('joi'); - -const { queries } = require('./constants'); -const VirtualType = require('./virtualType'); -const { ValidationError, errorCodes } = require('./errors'); - -const IS_QUERY_HOOK = { - update: true, - delete: true, - findOne: true, -}; - -const reserved = { - _events: true, - _eventsCount: true, - _lazySetupHooks: true, - _maxListeners: true, - _posts: true, - _pres: true, - className: true, - constructor: true, - delete: true, - domain: true, - ds: true, - emit: true, - entityData: true, - entityKey: true, - errors: true, - excludeFromIndexes: true, - get: true, - getEntityDataWithVirtuals: true, - gstore: true, - hook: true, - init: true, - isModified: true, - isNew: true, - listeners: true, - model: true, - modelName: true, - on: true, - once: true, - plain: true, - post: true, - pre: true, - removeListener: true, - removePost: true, - removePre: true, - save: true, - schema: true, - set: true, - toObject: true, - update: true, - validate: true, -}; - -class Schema { - constructor(properties, options) { - const self = this; - - this.instanceOfSchema = true; - this.methods = {}; - this.statics = {}; - this.virtuals = {}; - this.shortcutQueries = {}; - this.paths = {}; - this.callQueue = { - model: {}, - entity: {}, - }; - - this.options = defaultOptions(options); - - Object.keys(properties).forEach(k => { - if (reserved[k]) { - throw new Error(`${k} is reserved and can not be used as a schema pathname`); - } - - self.paths[k] = properties[k]; - }); - - // defaultMiddleware.forEach((m) => { - // self[m.kind](m.hook, m.fn); - // }); - - if (options) { - this._joi = buildJoiSchema(properties, this.options.joi); - } - } - - method(name, fn) { - const self = this; - if (typeof name !== 'string') { - if (typeof name !== 'object') { - return; - } - Object.keys(name).forEach(k => { - if (typeof name[k] === 'function') { - self.methods[k] = name[k]; - } - }); - } else if (typeof fn === 'function') { - this.methods[name] = fn; - } - } - - queries(type, settings) { - this.shortcutQueries[type] = settings; - } - - path(propName, definition) { - if (typeof definition === 'undefined') { - if (this.paths[propName]) { - return this.paths[propName]; - } - return undefined; - } - - if (reserved[propName]) { - throw new Error(`${propName} is reserved and can not be used as a schema pathname`); - } - - this.paths[propName] = definition; - return this; - } - - pre(method, fn) { - const queue = IS_QUERY_HOOK[method] ? this.callQueue.model : this.callQueue.entity; - - if (!{}.hasOwnProperty.call(queue, method)) { - queue[method] = { - pres: [], - post: [], - }; - } - - return queue[method].pres.push(fn); - } - - post(method, fn) { - const queue = IS_QUERY_HOOK[method] ? this.callQueue.model : this.callQueue.entity; - - if (!{}.hasOwnProperty.call(queue, method)) { - queue[method] = { - pres: [], - post: [], - }; - } - - return queue[method].post.push(fn); - } - - virtual(propName) { - if (reserved[propName]) { - throw new Error(`${propName} is reserved and can not be used as virtual property.`); - } - if (!{}.hasOwnProperty.call(this.virtuals, propName)) { - this.virtuals[propName] = new VirtualType(propName); - } - return this.virtuals[propName]; - } - - validateJoi(entityData) { - if (!this.isJoi) { - return { - error: new ValidationError( - errorCodes.ERR_GENERIC, - 'Schema does not have a joi configuration object' - ), - value: entityData, - }; - } - return this._joi.validate(entityData, this.options.joi.options || {}); - } - - get isJoi() { - return !is.undef(this._joi); - } -} - -/** - * Static properties - */ -Schema.Types = { - Double: 'double', - GeoPoint: 'geoPoint', - Key: 'entityKey', -}; - -/** - * Merge options passed with the default option for Schemas - * @param options - */ -function defaultOptions(options) { - const optionsDefault = { - validateBeforeSave: true, - explicitOnly: true, - excludeLargeProperties: false, - queries: { - readAll: false, - format: queries.formats.JSON, - }, - }; - options = extend(true, {}, optionsDefault, options); - if (options.joi) { - const joiOptionsDefault = { - options: { - allowUnknown: options.explicitOnly !== true, - }, - }; - if (is.object(options.joi)) { - options.joi = extend(true, {}, joiOptionsDefault, options.joi); - } else { - options.joi = { ...joiOptionsDefault }; - } - if (!Object.prototype.hasOwnProperty.call(options.joi.options, 'stripUnknown')) { - options.joi.options.stripUnknown = options.joi.options.allowUnknown !== true; - } - } - return options; -} - -function buildJoiSchema(schema, joiConfig) { - if (!is.object(joiConfig)) { - return undefined; - } - - const hasExtra = is.object(joiConfig.extra); - const joiKeys = {}; - - Object.keys(schema).forEach(k => { - if ({}.hasOwnProperty.call(schema[k], 'joi')) { - joiKeys[k] = schema[k].joi; - } - }); - - let joiSchema = Joi.object().keys(joiKeys); - let args; - - if (hasExtra) { - Object.keys(joiConfig.extra).forEach(k => { - if (is.function(joiSchema[k])) { - args = joiConfig.extra[k]; - joiSchema = joiSchema[k].apply(joiSchema, args); - } - }); - } - - return joiSchema; -} - -module.exports = Schema; diff --git a/lib/serializer.js b/lib/serializer.js deleted file mode 100644 index ae1841b..0000000 --- a/lib/serializer.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const datastoreSerializer = require('./serializers/datastore'); - -exports.Datastore = datastoreSerializer; diff --git a/lib/serializers/datastore.js b/lib/serializers/datastore.js deleted file mode 100644 index 44c525b..0000000 --- a/lib/serializers/datastore.js +++ /dev/null @@ -1,149 +0,0 @@ -'use strict'; - -const is = require('is'); -const arrify = require('arrify'); - -function toDatastore(entity, options = {}) { - // For now the more robust excudeFromIndexes Array declaration - // has an issue with Arrays ("Exclude from indexes cannot be set on a list value") - // and cannot be used for now - // See issue: https://github.com/googleapis/nodejs-datastore/issues/14 - - const data = Object.entries(entity.entityData).reduce((acc, [key, value]) => { - if (typeof value !== 'undefined') { - acc[key] = value; - } - return acc; - }, {}); - - const excludeFromIndexes = getExcludeFromIndexes(); - - const datastoreFormat = { - key: entity.entityKey, - data, - excludeLargeProperties: entity.schema.options.excludeLargeProperties, - }; - - if (excludeFromIndexes.length > 0) { - datastoreFormat.excludeFromIndexes = excludeFromIndexes; - } - - if (options.method) { - datastoreFormat.method = options.method; - } - - return datastoreFormat; - - // --------- - - function getExcludeFromIndexes() { - return Object.entries(data) - .filter(({ 1: value }) => value !== null) - .map(([key]) => entity.excludeFromIndexes[key]) - .filter(v => v !== undefined) - .reduce((acc, arr) => [...acc, ...arr], []); - } -} - -function fromDatastore(entity, options = {}) { - switch (options.format) { - case 'ENTITY': - return convertToEntity.call(this); - default: - return convertToJson.call(this); - } - - // -------------- - - function convertToJson() { - options.readAll = typeof options.readAll === 'undefined' ? false : options.readAll; - - const { schema } = this; - const { KEY } = this.gstore.ds; - const entityKey = entity[KEY]; - const data = { - id: idFromKey(entityKey), - }; - data[KEY] = entityKey; - - Object.keys(entity).forEach(k => { - if (options.readAll || !{}.hasOwnProperty.call(schema.paths, k) || schema.paths[k].read !== false) { - let value = entity[k]; - - if ({}.hasOwnProperty.call(this.schema.paths, k)) { - // During queries @google-cloud converts datetime to number - if (this.schema.paths[k].type === 'datetime' && is.number(value)) { - value = new Date(value / 1000); - } - - // Sanitise embedded objects - if (typeof this.schema.paths[k].excludeFromRead !== 'undefined' - && is.array(this.schema.paths[k].excludeFromRead) - && !options.readAll) { - this.schema.paths[k].excludeFromRead.forEach(prop => { - const segments = prop.split('.'); - let v = value; - - while (segments.length > 1 && v !== undefined) { - v = v[segments.shift()]; - } - - const segment = segments.pop(); - - if (v !== undefined && segment in v) { - delete v[segment]; - } - }); - } - } - - data[k] = value; - } - }); - - if (options.showKey) { - data.__key = entityKey; - } else { - delete data.__key; - } - - return data; - - // ---------------------- - - function idFromKey(key) { - return key.path[key.path.length - 1]; - } - } - - function convertToEntity() { - const key = entity[this.gstore.ds.KEY]; - return this.__model(entity, null, null, null, key); - } -} - -/** - * Convert one or several entities instance (gstore) to Datastore format - * - * @param {any} entities Entity(ies) to format - * @returns {array} the formated entity(ies) - */ -function entitiesToDatastore(entities, options) { - const multiple = is.array(entities); - entities = arrify(entities); - - if (entities[0].className !== 'Entity') { - // Not an entity instance, nothing to do here... - return entities; - } - - const result = entities.map(e => toDatastore(e, options)); - - return multiple ? result : result[0]; -} - -module.exports = { - toDatastore, - fromDatastore, - entitiesToDatastore, -}; diff --git a/lib/virtualType.js b/lib/virtualType.js deleted file mode 100644 index a440052..0000000 --- a/lib/virtualType.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const is = require('is'); - -class VirtualType { - constructor(name, options) { - this.name = name; - this.getter = null; - this.setter = null; - this.options = options || {}; - } - - get(fn) { - if (!is.fn(fn)) { - throw new Error('You need to pass a function to virtual get'); - } - this.getter = fn; - return this; - } - - set(fn) { - if (!is.fn(fn)) { - throw new Error('You need to pass a function to virtual set'); - } - this.setter = fn; - return this; - } - - applyGetters(scope) { - if (this.getter === null) { - return null; - } - const v = this.getter.call(scope); - scope[this.name] = v; - return v; - } - - applySetters(value, scope) { - if (this.setter === null) { - return null; - } - const v = this.setter.call(scope, value); - return v; - } -} - -module.exports = VirtualType; diff --git a/package.json b/package.json index 550c20c..f3b435e 100755 --- a/package.json +++ b/package.json @@ -2,17 +2,24 @@ "name": "gstore-node", "version": "7.1.0", "description": "Google Datastore Entities Modeling for Node.js. Validate the Entity properties and type before saving to the Datastore. Advanced cache to speed up entities fetching.", - "main": "index.js", + "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { + "build": "rm -rf ./lib && tsc", + "postbuild": "bash scripts/post_build.sh", "commit": "git-cz", - "coverage": "rm -rf ./coverage && nyc report --reporter=html", "coveralls": "nyc report --reporter=text-lcov | coveralls", + "cover": "yarn cover:unit && yarn cover:integration && yarn cover:report", + "cover:unit": "DATASTORE_EMULATOR_HOST=localhost:8081 nyc --exclude-after-remap false yarn test:unit", + "cover:integration": "nyc --silent --no-clean yarn test:integration", + "cover:report": "nyc report --reporter=lcov --reporter=html", + "lint": "./node_modules/eslint/bin/eslint.js ./src ./test", "local-datastore": "gcloud beta emulators datastore start --consistency=1.0 --no-store-on-disk", - "lint": "./node_modules/eslint/bin/eslint.js ./lib ./test", - "pretest": "npm run lint", + "pretest": "yarn lint", "release": "standard-version", - "test": "DATASTORE_EMULATOR_HOST=localhost:8081 nyc mocha test --recursive --colors" + "test": "yarn test:unit && yarn test:integration", + "test:unit": "DATASTORE_EMULATOR_HOST=localhost:8081 mocha test --exclude test/integration/**/*.* --recursive --colors --exit", + "test:integration": "DATASTORE_EMULATOR_HOST=localhost:8081 mocha test/integration --recursive --colors --exit" }, "engines": { "node": ">=10.0" @@ -46,12 +53,6 @@ "url": "https://github.com/jfbenckhuijsen" } ], - "nyc": { - "reporter": [ - "lcov", - "text" - ] - }, "license": "MIT", "config": { "commitizen": { @@ -74,8 +75,16 @@ }, "devDependencies": { "@google-cloud/datastore": "^4.0.0", - "babel-cli": "^6.26.0", - "babel-preset-es2015": "^6.24.1", + "@hapi/joi": "^15.1.1", + "@types/arrify": "2.0.1", + "@types/extend": "^3.0.1", + "@types/is": "^0.0.21", + "@types/jest": "^24.0.18", + "@types/lodash.get": "^4.4.6", + "@types/lodash.set": "^4.3.6", + "@types/validator": "^10.11.3", + "@typescript-eslint/eslint-plugin": "^2.2.0", + "@typescript-eslint/parser": "^2.2.0", "cache-manager-redis-store": "^1.5.0", "chai": "^4.2.0", "chance": "^1.1.0", @@ -84,18 +93,22 @@ "cz-conventional-changelog": "^3.0.2", "eslint": "^6.3.0", "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.3.0", "eslint-import-resolver-webpack": "^0.11.1", "eslint-plugin-import": "^2.18.2", "eslint-plugin-mocha": "^6.1.0", - "@hapi/joi": "^15.1.1", + "eslint-plugin-prettier": "^3.1.0", + "jest": "^24.9.0", "mocha": "^6.2.0", "mocha-lcov-reporter": "^1.3.0", "nconf": "^0.10.0", "nyc": "^14.1.1", + "prettier": "^1.18.2", "redis-mock": "^0.46.0", "rimraf": "3.0.0", "sinon": "^7.4.2", "standard-version": "^7.0.0", + "typescript": "^3.6.3", "yargs": "^14.0.0" }, "peerDependencies": { diff --git a/scripts/post_build.sh b/scripts/post_build.sh new file mode 100644 index 0000000..c0ccf52 --- /dev/null +++ b/scripts/post_build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +rm lib/package.json +mv $(pwd)/lib/src/* $(pwd)/lib +rm -rf lib/src diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 0000000..9e4d972 --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,34 @@ +module.exports = { + parser: '@typescript-eslint/parser', + root: true, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + project: './tsconfig.json', + }, + extends: [ + '../.eslintrc.js', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier/@typescript-eslint', + 'plugin:prettier/recommended', + ], + env: { + node: true, + mocha: true, + }, + plugins: ['@typescript-eslint'], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], + }, +}; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..8056d35 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +export const QUERIES_FORMATS = { + JSON: 'JSON', + ENTITY: 'ENTITY', +}; diff --git a/src/dataloader.ts b/src/dataloader.ts new file mode 100644 index 0000000..6f59edc --- /dev/null +++ b/src/dataloader.ts @@ -0,0 +1,48 @@ +import optional from 'optional'; +import arrify from 'arrify'; +import datastoreAdapterFactory from 'nsql-cache-datastore'; +import { Datastore } from '@google-cloud/datastore'; +import DataLoader from 'dataloader'; // eslint-disable-line import/no-extraneous-dependencies + +import { EntityKey, EntityData } from './types'; + +const OptionalDataloader = optional('dataloader'); + +const dsAdapter = datastoreAdapterFactory(); + +const { keyToString } = dsAdapter; + +/** + * Create a DataLoader instance + * @param {Datastore} ds @google-cloud Datastore instance + */ +export const createDataLoader = ( + ds: Datastore, + options?: { maxBatchSize: number }, +): DataLoader => { + if (!ds) { + throw new Error('A Datastore instance has to be passed'); + } + + const fetchHandler = (keys: EntityKey[]): Promise => + ds.get(keys).then(([response]: [EntityData | EntityData[]]) => { + // When providing an Array with 1 Key item, google-datastore + // returns a single item. + // For predictable results in gstore, all responses from Datastore.get() + // calls return an Array + const entityData = arrify(response); + const entitiesByKey: { [key: string]: any } = {}; + entityData.forEach(data => { + entitiesByKey[keyToString(data[ds.KEY as any])] = data; + }); + + return keys.map(key => entitiesByKey[keyToString(key)] || null); + }); + + const defaultOptions = { + cacheKeyFn: (key: EntityKey): string => keyToString(key), + maxBatchSize: 1000, + }; + + return new OptionalDataloader(fetchHandler, { ...defaultOptions, ...options }); +}; diff --git a/src/entity.ts b/src/entity.ts new file mode 100644 index 0000000..c7c44a7 --- /dev/null +++ b/src/entity.ts @@ -0,0 +1,619 @@ +import is from 'is'; +import hooks from 'promised-hooks'; +import arrify from 'arrify'; +import { Transaction } from '@google-cloud/datastore'; +import DataLoader from 'dataloader'; + +import defaultValues from './helpers/defaultValues'; +import helpers from './helpers'; +import Gstore from './index'; +import Schema, { SchemaPathDefinition } from './schema'; +import Model from './model'; +import { datastoreSerializer } from './serializers'; +import { ERROR_CODES, ValidationError } from './errors'; +import { + EntityKey, + EntityData, + IdType, + Ancestor, + GenericObject, + DatastoreSaveMethod, + PopulateRef, + PromiseWithPopulate, +} from './types'; +import { ValidateResponse } from './helpers/validation'; +import { PopulateHandler } from './helpers/populateHelpers'; + +const { validation, populateHelpers } = helpers; +const { populateFactory } = populateHelpers; + +export class Entity { + /* The entity Key */ + public entityKey: EntityKey; + + /* The entity data */ + public entityData: { [P in keyof T]: T[P] } = {} as any; + + /** + * If you provided a dataloader instance when saving the entity, it will + * be added as property. You will then have access to it in your "pre" save() hooks. + */ + public dataloader: DataLoader | undefined; + + public context: GenericObject; + + public __gstore: Gstore | undefined; // Added when creating the Model + + public __schema: Schema | undefined; // Added when creating the Model + + public __entityKind: string | undefined; // Added when creating the Model + + public __className: string; + + public __excludeFromIndexes: { [P in keyof T]?: string[] }; + + public __hooksEnabled = true; + + constructor(data: EntityData, id?: IdType, ancestors?: Ancestor, namespace?: string, key?: EntityKey) { + this.__className = 'Entity'; + + this.__excludeFromIndexes = {}; + + /** + * Object to store custom data for the entity. + * In some cases we might want to add custom data onto the entity + * and as Typescript won't allow random properties to be added, this + * is the place to add data based on the context. + */ + this.context = {}; + + if (key) { + if (!this.gstore.ds.isKey(key)) { + throw new Error('Entity Key must be a Datastore Key'); + } + this.entityKey = key; + } else { + this.entityKey = this.__createKey(id, ancestors, namespace); + } + + // create entityData from data provided + this.__buildEntityData(data || {}); + + this.__addAliasAndVirtualProperties(); + + this.__addCustomMethodsFromSchema(); + + // Wrap entity with hook "pre" and "post" methods + hooks.wrap(this); + + // Add the middlewares defined on the Schena + this.__registerHooksFromSchema(); + } + + /** + * Save the entity in the Datastore + * + * @param {Transaction} transaction The optional transaction to save the entity into + * @param options Additional configuration + * @returns {Promise>} + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/save + */ + save(transaction?: Transaction, opts?: SaveOptions): Promise> { + this.__hooksEnabled = true; + + const options = { + method: 'upsert' as DatastoreSaveMethod, + sanitizeEntityData: true, + ...opts, + } as SaveOptions; + + // ------------------------ HANDLERS --------------------------------- + // ------------------------------------------------------------------- + + const validateEntityData = (): Partial => { + if (this.schema.options.validateBeforeSave) { + return this.validate(); + } + + return { error: null }; + }; + + const validateMethod = (method: string): { error: Error | null } => { + const allowed: { [key: string]: boolean } = { + update: true, + insert: true, + upsert: true, + }; + + return !allowed[method] + ? { error: new Error('Method must be either "update", "insert" or "upsert"') } + : { error: null }; + }; + + const validateDataAndMethod = (): { error: ValidationError | Error | null } => { + const { error: entityDataError } = validateEntityData(); + let methodError: Error | null; + if (!entityDataError) { + ({ error: methodError } = validateMethod(options.method)); + } + + return { error: entityDataError || methodError! }; + }; + + /** + * Process some basic formatting to the entity data before save + * - automatically set the modifiedOn property to current date (if exists on schema) + * - convert object with latitude/longitude to Datastore GeoPoint + */ + const serializeData = (): EntityData => { + /** + * If the schema has a "modifiedOn" property we automatically + * update its value to the current dateTime + */ + if ({}.hasOwnProperty.call(this.schema.paths, 'modifiedOn')) { + (this.entityData as any).modifiedOn = new Date(); + } + + /** + * If the entityData has some property of type 'geoPoint' + * and its value is an js object with "latitude" and "longitude" + * we convert it to a datastore GeoPoint. + */ + if ({}.hasOwnProperty.call(this.schema.__meta, 'geoPointsProps')) { + this.schema.__meta.geoPointsProps.forEach((property: string) => { + if ( + {}.hasOwnProperty.call(this.entityData, property) && + (this.entityData as any)[property] !== null && + (this.entityData as any)[property].constructor.name !== 'GeoPoint' + ) { + (this.entityData as any)[property] = this.gstore.ds.geoPoint((this.entityData as any)[property]); + } + }); + } + return this.entityData; + }; + + const onEntitySaved = (): Promise> => { + /** + * Make sure to clear the cache for this Entity Kind + */ + if ((this.constructor as Model).__hasCache(options)) { + return (this.constructor as Model) + .clearCache() + .then(() => (this as unknown) as EntityResponse) + .catch((err: any) => { + let msg = 'Error while clearing the cache after saving the entity.'; + msg += 'The entity has been saved successfully though. '; + msg += 'Both the cache error and the entity saved have been attached.'; + const cacheError = new Error(msg); + (cacheError as any).__entity = this; + (cacheError as any).__cacheError = err; + throw cacheError; + }); + } + + return Promise.resolve((this as unknown) as EntityResponse); + }; + + /** + * If it is a transaction, we create a hooks.post array that will be executed + * when transaction succeeds by calling transaction.execPostHooks() (returns a Promises) + */ + const attachPostHooksToTransaction = (): void => { + // Disable the "post" hooks as we will only run them after the transaction succcees + this.__hooksEnabled = false; + (this.constructor as Model).__hooksTransaction.call( + this, + transaction!, + (this as any).__posts ? (this as any).__posts.save : undefined, + ); + }; + + // ------------------------ END HANDLERS -------------------------------- + + if (options.sanitizeEntityData) { + // this.entityData = (this.constructor as Model).sanitize.call(this.constructor, this.entityData, { + this.entityData = (this.constructor as Model).sanitize.call(this.constructor, this.entityData, { + disabled: ['write'], + }); + } + + const { error } = validateDataAndMethod(); + if (error) { + return Promise.reject(error); + } + + this.entityData = serializeData(); + + const datastoreEntity = datastoreSerializer.toDatastore(this); + datastoreEntity.method = options.method; + + if (transaction) { + if (transaction.constructor.name !== 'Transaction') { + return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); + } + + attachPostHooksToTransaction(); + transaction.save(datastoreEntity); + + return Promise.resolve((this as unknown) as EntityResponse); + } + + return this.gstore.ds.save(datastoreEntity).then(onEntitySaved); + } + + /** + * Validate the entity data. It returns an object with an `error` and a `value` property. + * If the error is `null`, the validation has passed. + * The `value` returned is the entityData sanitized (unknown properties removed). + * + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/validate + */ + validate(): ValidateResponse { + const { entityData, schema, entityKind, gstore } = this; + + return validation.validate(entityData, schema, entityKind, gstore.ds); + } + + /** + * Returns a JSON object of the entity data along with the entity id/name. + * The properties on the Schema where "read" has been set to "false" won't be added + * unless `readAll: true` is passed in the options. + * + * @param options Additional configuration + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/plain + */ + plain(options: PlainOptions | undefined = {}): Partial> { + if (!is.object(options)) { + throw new Error('Options must be an Object'); + } + const readAll = !!options.readAll || false; + const virtuals = !!options.virtuals || false; + const showKey = !!options.showKey || false; + + if (virtuals) { + // Add any possible virtual properties to the object + this.entityData = this.__getEntityDataWithVirtuals(); + } + + const data = datastoreSerializer.fromDatastore(this.entityData, this.constructor as Model, { + readAll, + showKey, + }); + + return data; + } + + get

(path: P): any { + if ({}.hasOwnProperty.call(this.schema.__virtuals, path)) { + return this.schema.__virtuals[path as string].applyGetters(this.entityData); + } + return this.entityData[path]; + } + + set

(path: P, value: any): EntityResponse { + if ({}.hasOwnProperty.call(this.schema.__virtuals, path)) { + this.schema.__virtuals[path as string].applySetters(value, this.entityData); + return (this as unknown) as EntityResponse; + } + + this.entityData[path] = value; + return (this as unknown) as EntityResponse; + } + + /** + * Access any gstore Model from the entity instance. + * + * @param {string} entityKind The entity kind + * @returns {Model} The Model + * @example + ``` + const user = new User({ name: 'john', pictId: 123}); + const ImageModel = user.model('Image'); + ImageModel.get(user.pictId).then(...); + ``` + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/model + */ + model(name: string): Model { + return this.gstore.model(name); + } + + // TODO: Rename this function "fetch" (and create alias to this for backward compatibility) + /** + * Fetch entity from Datastore + * + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/datastoreentity + */ + datastoreEntity(options = {}): Promise | null> { + const onEntityFetched = (result: [EntityData | null]): EntityResponse | null => { + const entityData = result ? result[0] : null; + + if (!entityData) { + if (this.gstore.config.errorOnEntityNotFound) { + const error = new Error('Entity not found'); + (error as any).code = ERROR_CODES.ERR_ENTITY_NOT_FOUND; + throw error; + } + + return null; + } + + this.entityData = entityData; + return (this as unknown) as EntityResponse; + }; + + if ((this.constructor as Model).__hasCache(options)) { + return this.gstore.cache!.keys.read(this.entityKey, options).then(onEntityFetched); + } + return this.gstore.ds.get(this.entityKey).then(onEntityFetched); + } + + /** + * Populate entity references (whose properties are an entity Key) and merge them in the entity data. + * + * @param refs The entity references to fetch from the Datastore. Can be one (string) or multiple (array of string) + * @param properties The properties to return from the reference entities. If not specified, all properties will be returned + * @link https://sebloix.gitbook.io/gstore-node/entity/methods/populate + */ + populate( + path?: U, + propsToSelect?: U extends Array ? never : string | string[], + ): PromiseWithPopulate> { + const refsToPopulate: PopulateRef[][] = []; + + const promise = Promise.resolve(this).then((this.constructor as Model).__populate(refsToPopulate)); + + ((promise as any).populate as PopulateHandler) = populateFactory(refsToPopulate, promise, this.schema); + ((promise as any).populate as PopulateHandler)(path, propsToSelect); + return promise as any; + } + + get id(): string | number { + return this.entityKey.id || this.entityKey.name!; + } + + /** + * The gstore instance + */ + get gstore(): Gstore { + if (this.__gstore === undefined) { + throw new Error('No gstore instance attached to entity'); + } + return this.__gstore; + } + + /** + * The entity Model Schema + */ + get schema(): Schema { + if (this.__schema === undefined) { + throw new Error('No schema instance attached to entity'); + } + return this.__schema; + } + + /** + * The Datastore entity kind + */ + get entityKind(): string { + if (this.__entityKind === undefined) { + throw new Error('No entity kind attached to entity'); + } + return this.__entityKind; + } + + __buildEntityData(data: GenericObject): void { + const { schema } = this; + const isJoiSchema = schema.isJoi; + + // If Joi schema, get its default values + if (isJoiSchema) { + const { error, value } = schema.validateJoi(data); + + if (!error) { + this.entityData = { ...value }; + } + } + + this.entityData = { ...this.entityData, ...data }; + + let isArray; + let isObject; + + Object.entries(schema.paths as { [k: string]: SchemaPathDefinition }).forEach(([key, prop]) => { + const hasValue = {}.hasOwnProperty.call(this.entityData, key); + const isOptional = {}.hasOwnProperty.call(prop, 'optional') && prop.optional !== false; + const isRequired = {}.hasOwnProperty.call(prop, 'required') && prop.required === true; + + // Set Default Values + if (!isJoiSchema && !hasValue && !isOptional) { + let value = null; + + if ({}.hasOwnProperty.call(prop, 'default')) { + if (typeof prop.default === 'function') { + value = prop.default(); + } else { + value = prop.default; + } + } + + if ({}.hasOwnProperty.call(defaultValues.__map__, value)) { + /** + * If default value is in the gstore.defaultValue hashTable + * then execute the handler for that shortcut + */ + value = defaultValues.__handler__(value); + } else if (value === null && {}.hasOwnProperty.call(prop, 'values') && !isRequired) { + // Default to first value of the allowed values if **not** required + [value] = prop.values as any[]; + } + + this.entityData[key as keyof T] = value; + } + + // Set excludeFromIndexes + // ---------------------- + isArray = prop.type === Array || (prop.joi && prop.joi._type === 'array'); + isObject = prop.type === Object || (prop.joi && prop.joi._type === 'object'); + + if (prop.excludeFromIndexes === true) { + if (isArray) { + // We exclude both the array values + all the child properties of object items + this.__excludeFromIndexes[key as keyof T] = [`${key}[]`, `${key}[].*`]; + } else if (isObject) { + // We exclude the emmbeded entity + all its properties + this.__excludeFromIndexes[key as keyof T] = [key, `${key}.*`]; + } else { + this.__excludeFromIndexes[key as keyof T] = [key]; + } + } else if (prop.excludeFromIndexes !== false) { + const excludedArray = arrify(prop.excludeFromIndexes) as string[]; + if (isArray) { + // The format to exclude a property from an embedded entity inside + // an array is: "myArrayProp[].embeddedKey" + this.__excludeFromIndexes[key as keyof T] = excludedArray.map(propExcluded => `${key}[].${propExcluded}`); + } else if (isObject) { + // The format to exclude a property from an embedded entity + // is: "myEmbeddedEntity.key" + this.__excludeFromIndexes[key as keyof T] = excludedArray.map(propExcluded => `${key}.${propExcluded}`); + } + } + }); + + // add Symbol Key to the entityData + (this.entityData as any)[this.gstore.ds.KEY] = this.entityKey; + } + + __createKey(id?: IdType, ancestors?: Ancestor, namespace?: string): EntityKey { + if (id && !is.number(id) && !is.string(id)) { + throw new Error('id must be a string or a number'); + } + + const hasAncestors = typeof ancestors !== 'undefined' && ancestors !== null && is.array(ancestors); + + let path: (string | number)[] = hasAncestors ? [...ancestors!] : []; + + if (id) { + path = [...path, this.entityKind, id]; + } else { + path.push(this.entityKind); + } + + return namespace ? this.gstore.ds.key({ namespace, path }) : this.gstore.ds.key(path); + } + + __addAliasAndVirtualProperties(): void { + const { schema } = this; + + // Create virtual properties (getters and setters for entityData object) + Object.keys(schema.paths) + .filter(pathKey => ({}.hasOwnProperty.call(schema.paths, pathKey))) + .forEach(pathKey => + Object.defineProperty(this, pathKey, { + get: function getProp() { + return this.entityData[pathKey]; + }, + set: function setProp(newValue) { + this.entityData[pathKey] = newValue; + }, + }), + ); + + // Create virtual properties (getters and setters for "virtuals" defined on the Schema) + + Object.keys(schema.__virtuals) + .filter(key => ({}.hasOwnProperty.call(schema.__virtuals, key))) + .forEach(key => + Object.defineProperty(this, key, { + get: function getProp() { + return schema.__virtuals[key].applyGetters({ ...this.entityData }); + }, + set: function setProp(newValue) { + schema.__virtuals[key].applySetters(newValue, this.entityData); + }, + }), + ); + } + + __registerHooksFromSchema(): Entity { + const callQueue = this.schema.__callQueue.entity; + + if (!Object.keys(callQueue).length) { + return this; + } + + Object.keys(callQueue).forEach(method => { + if (!(this as any)[method]) { + return; + } + + // Add Pre hooks + callQueue[method].pres.forEach(fn => { + (this as any).pre(method, fn); + }); + + // Add Pre hooks + callQueue[method].post.forEach(fn => { + (this as any).post(method, fn); + }); + }); + + return this; + } + + __addCustomMethodsFromSchema(): void { + Object.entries(this.schema.methods).forEach(([method, handler]) => { + (this as any)[method] = handler; + }); + } + + __getEntityDataWithVirtuals(): EntityData & { [key: string]: any } { + const { __virtuals } = this.schema; + const entityData: EntityData & { [key: string]: any } = { ...this.entityData }; + + Object.keys(__virtuals).forEach(k => { + if ({}.hasOwnProperty.call(entityData, k)) { + __virtuals[k].applySetters(entityData[k], entityData); + } else { + __virtuals[k].applyGetters(entityData); + } + }); + + return entityData; + } +} + +export default Entity; + +export type EntityResponse = Entity & T; + +interface SaveOptions { + method: DatastoreSaveMethod; + sanitizeEntityData: boolean; + cache?: any; +} + +interface PlainOptions { + /** + * Output all the entity data properties, regardless of the Schema `read` setting. + * + * @type {boolean} + * @default false + */ + readAll?: boolean; + /** + * Add the _virtual_ properties defined for the entity on the Schema. + * + * @type {boolean} + * @default false + * @link https://sebloix.gitbook.io/gstore-node/schema/methods/virtual + */ + virtuals?: boolean; + /** + * Add the full entity _Key_ object at the a "__key" property + * + * @type {boolean} + * @default false + */ + showKey?: boolean; +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100755 index 0000000..19ed279 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,88 @@ +/* eslint-disable max-classes-per-file, no-use-before-define */ + +import util from 'util'; +import is from 'is'; + +type MessageGetter = (...args: any[]) => string; + +export const ERROR_CODES = { + ERR_ENTITY_NOT_FOUND: 'ERR_ENTITY_NOT_FOUND', + ERR_GENERIC: 'ERR_GENERIC', + ERR_VALIDATION: 'ERR_VALIDATION', + ERR_PROP_TYPE: 'ERR_PROP_TYPE', + ERR_PROP_VALUE: 'ERR_PROP_VALUE', + ERR_PROP_NOT_ALLOWED: 'ERR_PROP_NOT_ALLOWED', + ERR_PROP_REQUIRED: 'ERR_PROP_REQUIRED', + ERR_PROP_IN_RANGE: 'ERR_PROP_IN_RANGE', +}; + +export const message = (text: string, ...args: any[]): string => util.format(text, ...args); + +const messages: { [key: string]: string | (MessageGetter) } = { + ERR_GENERIC: 'An error occured', + ERR_VALIDATION: (entityKind: string) => + message('The entity data does not validate against the "%s" Schema', entityKind), + ERR_PROP_TYPE: (prop, type) => message('Property "%s" must be a %s', prop, type), + ERR_PROP_VALUE: (value, prop) => message('"%s" is not a valid value for property "%s"', value, prop), + ERR_PROP_NOT_ALLOWED: (prop, entityKind) => + message('Property "%s" is not allowed for entityKind "%s"', prop, entityKind), + ERR_PROP_REQUIRED: prop => message('Property "%s" is required but no value has been provided', prop), + ERR_PROP_IN_RANGE: (prop, range) => message('Property "%s" must be one of [%s]', prop, range && range.join(', ')), +}; + +export class GstoreError extends Error { + public code: string; + + constructor(code: string, msg?: string, args?: any) { + if (!msg && code && code in messages) { + if (is.function(messages[code])) { + msg = (messages[code] as MessageGetter)(...args.messageParams); + } else { + msg = messages[code] as string; + } + } + + if (!msg) { + msg = messages.ERR_GENERIC as string; + } + + super(msg); + this.name = 'GstoreError'; + this.message = msg; + this.code = code || ERROR_CODES.ERR_GENERIC; + + if (args) { + Object.keys(args).forEach(k => { + if (k !== 'messageParams') { + (this as any)[k] = args[k]; + } + }); + } + + Error.captureStackTrace(this, this.constructor); + } +} + +export class ValidationError extends GstoreError { + constructor(code: string, msg?: string, args?: any) { + super(code, msg, args); + this.name = 'ValidationError'; + Error.captureStackTrace(this, this.constructor); + } +} + +export class TypeError extends GstoreError { + constructor(code: string, msg?: string, args?: any) { + super(code, msg, args); + this.name = 'TypeError'; + Error.captureStackTrace(this, this.constructor); + } +} + +export class ValueError extends GstoreError { + constructor(code: string, msg?: string, args?: any) { + super(code, msg, args); + this.name = 'ValueError'; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/helpers/defaultValues.ts b/src/helpers/defaultValues.ts new file mode 100644 index 0000000..b8249ce --- /dev/null +++ b/src/helpers/defaultValues.ts @@ -0,0 +1,29 @@ +const NOW = 'CURRENT_DATETIME'; + +const returnCurrentTime = (): Date => new Date(); + +const mapDefaultValueIdToHandler: Record void> = { + [NOW]: returnCurrentTime, +}; + +const handler = (key: string): unknown => { + if ({}.hasOwnProperty.call(mapDefaultValueIdToHandler, key)) { + return mapDefaultValueIdToHandler[key](); + } + + return null; +}; + +export interface DefaultValues { + NOW: 'CURRENT_DATETIME'; + __handler__: (key: string) => unknown; + __map__: { [key: string]: () => any }; +} + +const defaultValues: DefaultValues = { + NOW, + __handler__: handler, + __map__: mapDefaultValueIdToHandler, +}; + +export default defaultValues; diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 0000000..fbdd51c --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,9 @@ +import queryHelpers from './queryhelpers'; +import validation from './validation'; +import populateHelpers from './populateHelpers'; + +export default { + queryHelpers, + validation, + populateHelpers, +}; diff --git a/src/helpers/populateHelpers.ts b/src/helpers/populateHelpers.ts new file mode 100644 index 0000000..339bdc8 --- /dev/null +++ b/src/helpers/populateHelpers.ts @@ -0,0 +1,92 @@ +import arrify from 'arrify'; + +import Schema, { SchemaPathDefinition } from '../schema'; +import { PopulateRef } from '../types'; + +/** + * Returns all the schema properties that are references + * to other entities (their value is an entity Key) + */ +const getEntitiesRefsFromSchema = (schema: Schema): string[] => + Object.entries(schema.paths) + .filter(([, pathConfig]) => (pathConfig as SchemaPathDefinition).type === 'entityKey') + .map(([property]) => property); + +/** + * + * @param {*} initialPath Path to add to the refs array + * @param {*} select Array of properties to select from the populated ref + * @param {*} refs Array of refs, each index is one level deep in the entityData tree + * + * @example + * + * const entityData = { + * user: Key, // ---> once fetched it will be { name: String, company: Key } + * address: Key + * } + * + * To fetch the "address", the "user" and the user's "conmpany", the array of refs + * to retrieve will have the following shape + * + * [ + * [{ path: 'user', select: ['*'] }, [ path: 'address', select: ['*'] ], // tree depth at level 0 + * [{ path: 'user.company', select: ['*'] }], // tree depth at level 1 (will be fetched after level 0 has been fetched) + * ] + */ +const addPathToPopulateRefs = ( + initialPath: string, + _select: string | string[] | never = ['*'], + refs: PopulateRef[][], +): void => { + const pathToArray = initialPath.split('.'); + const select = arrify(_select); + let prefix = ''; + + pathToArray.forEach((prop, i) => { + const currentPath = prefix ? `${prefix}.${prop}` : prop; + const nextPath = pathToArray[i + 1]; + const hasNextPath = typeof nextPath !== 'undefined'; + const refsAtCurrentTreeLevel = refs[i] || []; + + // Check if we alreday have a config for this tree level + const pathConfig = refsAtCurrentTreeLevel.find(ref => ref.path === currentPath); + + if (!pathConfig) { + refsAtCurrentTreeLevel.push({ path: currentPath, select: hasNextPath ? [nextPath] : select }); + } else if (hasNextPath && !pathConfig.select.some(s => s === nextPath)) { + // Add the next path to the selected properties on the ref + pathConfig.select.push(nextPath); + } else if (!hasNextPath && select.length) { + pathConfig.select.push(...select); + } + refs[i] = refsAtCurrentTreeLevel; + + prefix = currentPath; + }); +}; + +export type PopulateHandler = ( + path?: U, + propsToSelect?: U extends Array ? never : string | string[], +) => Promise; + +const populateFactory = ( + refsToPopulate: PopulateRef[][], + promise: Promise, + schema: Schema, +): PopulateHandler => { + const populateHandler: PopulateHandler = (path, propsToSelect) => { + if (propsToSelect && Array.isArray(path)) { + throw new Error('Only 1 property can be populated when fields to select are provided'); + } + + // If no path is specified, we fetch all the schema properties that are references to entities (Keys) + const paths: string[] = path ? arrify(path) : getEntitiesRefsFromSchema(schema); + paths.forEach(p => addPathToPopulateRefs(p, propsToSelect, refsToPopulate)); + return promise; + }; + + return populateHandler; +}; + +export default { addPathToPopulateRefs, populateFactory }; diff --git a/src/helpers/queryhelpers.ts b/src/helpers/queryhelpers.ts new file mode 100644 index 0000000..837b09e --- /dev/null +++ b/src/helpers/queryhelpers.ts @@ -0,0 +1,109 @@ +import { Datastore, Transaction, Query as DatastoreQuery } from '@google-cloud/datastore'; +import is from 'is'; +import arrify from 'arrify'; + +import { GstoreQuery, QueryListOptions } from '../query'; +import { EntityData } from '../types'; +import Model from '../model'; + +const buildQueryFromOptions = ( + query: GstoreQuery, Outputformat>, + options: QueryListOptions, + ds: Datastore, +): GstoreQuery, Outputformat> => { + if (!query || query.constructor.name !== 'Query') { + throw new Error('Query not passed'); + } + + if (!options || typeof options !== 'object') { + return query; + } + + if (options.limit) { + query.limit(options.limit); + } + + if (options.offset) { + query.offset(options.offset); + } + + if (options.order) { + const orderArray = arrify(options.order); + orderArray.forEach(order => { + query.order(order.property, { + descending: {}.hasOwnProperty.call(order, 'descending') ? order.descending : false, + }); + }); + } + + if (options.select) { + query.select(options.select); + } + + if (options.ancestors) { + if (!ds || ds.constructor.name !== 'Datastore') { + throw new Error('Datastore instance not passed'); + } + + const ancestorKey = options.namespace + ? ds.key({ namespace: options.namespace, path: options.ancestors.slice() }) + : ds.key(options.ancestors.slice()); + + query.hasAncestor(ancestorKey); + } + + if (options.filters) { + if (!is.array(options.filters)) { + throw new Error('Wrong format for filters option'); + } + + if (!is.array(options.filters[0])) { + options.filters = [options.filters]; + } + + if (options.filters[0].length > 1) { + options.filters.forEach(filter => { + // We check if the value is a function + // if it is, we execute it. + let value = filter[filter.length - 1]; + value = is.fn(value) ? value() : value; + const f = filter.slice(0, -1).concat([value]); + + (query.filter as any)(...f); + }); + } + } + + if (options.start) { + query.start(options.start); + } + + return query; +}; + +const createDatastoreQueryForModel = ( + model: Model, + namespace?: string, + transaction?: Transaction, +): DatastoreQuery => { + if (transaction && transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); + } + + const createQueryArgs: any[] = [model.entityKind]; + + if (namespace) { + createQueryArgs.unshift(namespace); + } + + if (transaction) { + return (transaction.createQuery as any)(...createQueryArgs); + } + + return model.gstore.ds.createQuery(...createQueryArgs); +}; + +export default { + buildQueryFromOptions, + createDatastoreQueryForModel, +}; diff --git a/src/helpers/validation.ts b/src/helpers/validation.ts new file mode 100755 index 0000000..0a27cc5 --- /dev/null +++ b/src/helpers/validation.ts @@ -0,0 +1,326 @@ +import moment from 'moment'; +import validator from 'validator'; +import is from 'is'; +import { Datastore } from '@google-cloud/datastore'; + +import { ValidationError, ValueError, TypeError, ERROR_CODES } from '../errors'; +import { EntityData } from '../types'; +import Schema, { SchemaPathDefinition, Validator } from '../schema'; + +const isValidDate = (value: any): boolean => { + if ( + value.constructor.name !== 'Date' && + (typeof value !== 'string' || + !/\d{4}-\d{2}-\d{2}([ ,T])?(\d{2}:\d{2}:\d{2})?(\.\d{1,3})?/.exec(value) || + !moment(value).isValid()) + ) { + return false; + } + return true; +}; + +const isInt = (n: unknown): boolean => Number(n) === n && n % 1 === 0; + +const isFloat = (n: unknown): boolean => Number(n) === n && n % 1 !== 0; + +const isValueEmpty = (v: unknown): boolean => + v === null || v === undefined || (typeof v === 'string' && v.trim().length === 0); + +const isValidLngLat = (data: any): boolean => { + const validLatitude = (isInt(data.latitude) || isFloat(data.latitude)) && data.latitude >= -90 && data.latitude <= 90; + const validLongitude = + (isInt(data.longitude) || isFloat(data.longitude)) && data.longitude >= -180 && data.longitude <= 180; + + return validLatitude && validLongitude; +}; + +const errorToObject = (error: any): { code: string; message: string; ref: string } => ({ + code: error.code, + message: error.message, + ref: error.ref, +}); + +const validatePropType = ( + value: any, + propType: unknown, + prop: unknown, + pathConfig: SchemaPathDefinition, + datastore: Datastore, +): TypeError | null => { + let isValid; + let ref; + let type = propType; + if (typeof propType === 'function') { + type = propType.name.toLowerCase(); + } + + switch (type) { + case 'entityKey': + isValid = datastore.isKey(value); + ref = 'key.base'; + if (isValid && pathConfig.ref) { + // Make sure the Entity Kind is also valid (if any) + const entityKind = value.path[value.path.length - 2]; + isValid = entityKind === pathConfig.ref; + ref = 'key.entityKind'; + } + break; + case 'string': + isValid = typeof value === 'string'; + ref = 'string.base'; + break; + case 'date': + isValid = isValidDate(value); + ref = 'datetime.base'; + break; + case 'array': + isValid = is.array(value); + ref = 'array.base'; + break; + case 'number': { + const isIntInstance = value.constructor.name === 'Int'; + if (isIntInstance) { + isValid = !isNaN(parseInt(value.value, 10)); + } else { + isValid = isInt(value); + } + ref = 'int.base'; + break; + } + case 'double': { + const isIntInstance = value.constructor.name === 'Double'; + if (isIntInstance) { + isValid = isFloat(parseFloat(value.value)) || isInt(parseFloat(value.value)); + } else { + isValid = isFloat(value) || isInt(value); + } + ref = 'double.base'; + break; + } + case 'buffer': + isValid = value instanceof Buffer; + ref = 'buffer.base'; + break; + case 'geoPoint': { + if ( + is.object(value) && + Object.keys(value).length === 2 && + {}.hasOwnProperty.call(value, 'longitude') && + {}.hasOwnProperty.call(value, 'latitude') + ) { + isValid = isValidLngLat(value); + } else { + isValid = value.constructor.name === 'GeoPoint'; + } + ref = 'geopoint.base'; + break; + } + default: + if (Array.isArray(value)) { + isValid = false; + } else { + isValid = typeof value === type; + } + ref = 'prop.type'; + } + + if (!isValid) { + return new TypeError(ERROR_CODES.ERR_PROP_TYPE, undefined, { ref, messageParams: [prop, type], property: prop }); + } + + return null; +}; + +const validatePropValue = (prop: string, value: any, validationRule?: Validator): ValueError | null => { + let validationArgs = []; + let validationFn: ((...args: any[]) => any) | undefined; + + /** + * If the validate property is an object, then it's assumed that + * it contains the 'rule' property, which will be the new + * validationRule's value. + * If the 'args' prop was passed then we concat them to 'validationArgs'. + */ + + if (typeof validationRule === 'object') { + const { rule } = validationRule; + validationArgs = validationRule.args || []; + + if (typeof rule === 'function') { + validationFn = rule; + validationArgs = [value, validator, ...validationArgs]; + } else { + validationRule = rule; + } + } + + if (!validationFn) { + /** + * Validator.js only works with string values + * let's make sure we are working with a string. + */ + const isObject = typeof value === 'object'; + const strValue = typeof value !== 'string' && !isObject ? String(value) : value; + validationArgs = [strValue, ...validationArgs]; + + validationFn = (validator as any)[validationRule as string]; + } + + if (!validationFn!.apply(validator, validationArgs)) { + return new ValueError(ERROR_CODES.ERR_PROP_VALUE, undefined, { + type: 'prop.validator', + messageParams: [value, prop], + property: prop, + }); + } + + return null; +}; + +export interface ValidateResponse { + error: ValidationError | null; + value: EntityData; + then: (onSuccess: (entityData: EntityData) => any, onError: (error: ValidationError) => any) => Promise; + catch: (onError: (error: ValidationError) => any) => Promise | undefined; +} + +const validate = ( + entityData: EntityData, + schema: Schema, + entityKind: string, + datastore: Datastore, +): ValidateResponse => { + const errors = []; + + let prop; + let skip; + let schemaHasProperty; + let pathConfig; + let propertyType; + let propertyValue; + let isEmpty; + let isRequired; + let error; + + const props = Object.keys(entityData); + const totalProps = Object.keys(entityData).length; + + if (schema.isJoi) { + // We leave the validation to Joi + return schema.validateJoi(entityData); + } + + for (let i = 0; i < totalProps; i += 1) { + prop = props[i]; + skip = false; + error = null; + schemaHasProperty = {}.hasOwnProperty.call(schema.paths, prop); + pathConfig = schema.paths[prop as keyof T] || {}; + propertyType = schemaHasProperty ? pathConfig.type : null; + propertyValue = entityData[prop]; + isEmpty = isValueEmpty(propertyValue); + + if (typeof propertyValue === 'string') { + propertyValue = propertyValue.trim(); + } + + if ({}.hasOwnProperty.call(schema.__virtuals, prop)) { + // Virtual, remove it and skip the rest + delete entityData[prop]; + skip = true; + } else if (!schemaHasProperty && schema.options.explicitOnly === false) { + // No more validation, key does not exist but it is allowed + skip = true; + } + + if (!skip) { + // ... is allowed? + if (!schemaHasProperty) { + error = new ValidationError(ERROR_CODES.ERR_PROP_NOT_ALLOWED, undefined, { + type: 'prop.not.allowed', + messageParams: [prop, entityKind], + property: prop, + }); + errors.push(errorToObject(error)); + } + + // ...is required? + isRequired = schemaHasProperty && {}.hasOwnProperty.call(pathConfig, 'required') && pathConfig.required === true; + + if (isRequired && isEmpty) { + error = new ValueError(ERROR_CODES.ERR_PROP_REQUIRED, undefined, { + type: 'prop.required', + messageParams: [prop], + property: prop, + }); + errors.push(errorToObject(error)); + } + + // ... is valid prop Type? + if (schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(pathConfig, 'type')) { + error = validatePropType(propertyValue, propertyType, prop, pathConfig, datastore); + + if (error) { + errors.push(errorToObject(error)); + } + } + + // ... is valid prop Value? + if (error === null && schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(pathConfig, 'validate')) { + error = validatePropValue(prop, propertyValue, pathConfig.validate); + if (error) { + errors.push(errorToObject(error)); + } + } + + // ... is value in range? + if ( + schemaHasProperty && + !isEmpty && + {}.hasOwnProperty.call(pathConfig, 'values') && + !pathConfig.values!.includes(propertyValue) + ) { + error = new ValueError(ERROR_CODES.ERR_PROP_IN_RANGE, undefined, { + type: 'value.range', + messageParams: [prop, pathConfig.values], + property: prop, + }); + + errors.push(errorToObject(error)); + } + } + } + + let validationError: ValidationError | null = null; + + if (Object.keys(errors).length > 0) { + validationError = new ValidationError(ERROR_CODES.ERR_VALIDATION, undefined, { + errors, + messageParams: [entityKind], + }); + } + + const validateResponse: ValidateResponse = { + error: validationError, + value: entityData, + then: (onSuccess, onError) => { + if (validationError) { + return Promise.resolve(onError(validationError)); + } + + return Promise.resolve(onSuccess(entityData)); + }, + catch: onError => { + if (validationError) { + return Promise.resolve(onError(validationError)); + } + return undefined; + }, + }; + + return validateResponse; +}; + +export default { + validate, +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ff89519 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,302 @@ +import is from 'is'; +import extend from 'extend'; +import hooks from 'promised-hooks'; +import NsqlCache, { NsqlCacheConfig } from 'nsql-cache'; +import dsAdapter from 'nsql-cache-datastore'; +import DataLoader from 'dataloader'; +import { Datastore, Transaction } from '@google-cloud/datastore'; +import pkg from '../package.json'; +import GstoreSchema from './schema'; +import GstoreEntity from './entity'; +import GstoreModel, { generateModel } from './model'; +import defaultValues, { DefaultValues } from './helpers/defaultValues'; +import { GstoreError, ValidationError, TypeError, ValueError, ERROR_CODES } from './errors'; +import { datastoreSerializer } from './serializers'; +import { createDataLoader } from './dataloader'; +import { EntityKey, EntityData, DatastoreSaveMethod, CustomEntityFunction } from './types'; + +export interface CacheConfig { + stores: any[]; + config: NsqlCacheConfig; +} + +export interface GstoreConfig { + cache?: boolean | CacheConfig; + /** + * If set to `true` (defaut), when fetching an entity by key and the entity is not found in the Datastore, + * gstore will throw an `"ERR_ENTITY_NOT_FOUND"` error. + * If set to `false`, `null` will be returned + */ + errorOnEntityNotFound?: boolean; +} +const DEFAULT_GSTORE_CONFIG = { + cache: undefined, + errorOnEntityNotFound: true, +}; + +const DEFAULT_CACHE_SETTINGS = { + config: { + wrapClient: false, + }, +}; + +export class Gstore { + /** + * Map of Gstore Model created + */ + public models: { [key: string]: GstoreModel }; + + /** + * Gstore Schema class + */ + public Schema: typeof GstoreSchema; + + /** + * Gstore instance configuration + */ + public config: GstoreConfig; + + /** + * The underlying gstore-cache instance + */ + public cache: NsqlCache | undefined; + + /** + * The symbol to access possible errors thrown + * in a "post" hooks + */ + public ERR_HOOKS: symbol; + + public errors: { + GstoreError: typeof GstoreError; + ValidationError: typeof ValidationError; + TypeError: typeof TypeError; + ValueError: typeof ValueError; + codes: typeof ERROR_CODES; + }; + + public __ds: Datastore | undefined; + + public __defaultValues: DefaultValues; + + public __pkgVersion = pkg.version; + + constructor(config: GstoreConfig = {}) { + if (!is.object(config)) { + throw new Error('Gstore config must be an object.'); + } + + this.models = {}; + this.config = { ...DEFAULT_GSTORE_CONFIG, ...config }; + this.Schema = GstoreSchema; + this.__defaultValues = defaultValues; + // this.__pkgVersion = pkg.version; + + this.errors = { + GstoreError, + ValidationError, + TypeError, + ValueError, + codes: ERROR_CODES, + }; + + this.ERR_HOOKS = hooks.ERRORS; + } + + /** + * Create or access a gstore Model + * + * @param {string} entityKind The Google Entity Kind + * @param {Schema} schema A gstore schema instance + * @returns {Model} A gstore Model + */ + model }>( + entityKind: string, + schema?: GstoreSchema, + ): GstoreModel { + if (this.models[entityKind]) { + // Don't allow overriding Model schema + if (schema instanceof GstoreSchema && schema !== undefined) { + throw new Error(`Trying to override ${entityKind} Model Schema`); + } + return this.models[entityKind]; + } + + if (!schema) { + throw new Error('A Schema needs to be provided to create a Model.'); + } + + const model = generateModel(entityKind, schema, this); + + this.models[entityKind] = model; + + return this.models[entityKind]; + } + + /** + * Initialize a @google-cloud/datastore Transaction + */ + transaction(): Transaction { + return this.ds.transaction(); + } + + /** + * Return an array of model names created on this instance of Gstore + * @returns {Array} + */ + modelNames(): string[] { + const names = Object.keys(this.models); + return names; + } + + /** + * Alias to the underlying @google-cloud/datastore `save()` method + * but instead of passing entity _keys_ this methods accepts one or multiple gstore **_entity_** instance(s). + * + * @param {(Entity | Entity[])} entity The entity(ies) to delete (any Entity Kind). Can be one or many (Array). + * @param {Transaction} [transaction] An Optional transaction to save the entities into + * @returns {Promise} + * @link https://sebloix.gitbook.io/gstore-node/gstore-methods/save + */ + save( + entities: GstoreEntity | GstoreEntity[], + transaction?: Transaction, + options: { method?: DatastoreSaveMethod; validate?: boolean } | undefined = {}, + ): Promise< + [ + { + mutationResults?: any; + indexUpdates?: number | null; + }, + ] + > { + if (!entities) { + throw new Error('No entities passed'); + } + + // Validate entities before saving + if (options.validate) { + let error; + const validateEntity = (entity: GstoreEntity): void => { + ({ error } = entity.validate()); + if (error) { + throw error; + } + }; + try { + if (Array.isArray(entities)) { + entities.forEach(validateEntity); + } else { + validateEntity(entities); + } + } catch (err) { + return Promise.reject(err); + } + } + + // Convert gstore entities to datastore forma ({key, data}) + const entitiesSerialized = datastoreSerializer.entitiesToDatastore(entities, options); + + if (transaction) { + return transaction.save(entitiesSerialized); + } + + // We forward the call to google-datastore + return this.ds.save(entitiesSerialized); + } + + /** + * Connect gstore node to the Datastore instance + * + * @param {Datastore} datastore A Datastore instance + */ + connect(datastore: any): void { + if (!datastore.constructor || datastore.constructor.name !== 'Datastore') { + throw new Error('No @google-cloud/datastore instance provided.'); + } + + this.__ds = datastore; + + if (this.config.cache) { + const cacheSettings = + this.config.cache === true + ? extend(true, {}, DEFAULT_CACHE_SETTINGS) + : extend(true, {}, DEFAULT_CACHE_SETTINGS, this.config.cache); + + const { stores, config } = cacheSettings as CacheConfig; + + const db = dsAdapter(datastore); + this.cache = new NsqlCache({ db, stores, config }); + delete this.config.cache; + } + } + + /** + * Create a DataLoader instance. + * Follow the link below for more info about Dataloader. + * + * @returns {DataLoader} The DataLoader instance + * @link https://sebloix.gitbook.io/gstore-node/cache-dataloader/dataloader + */ + createDataLoader(): DataLoader { + return createDataLoader(this.ds); + } + + /** + * Default values for schema properties + */ + get defaultValues(): DefaultValues { + return this.__defaultValues; + } + + get version(): string { + return this.__pkgVersion; + } + + /** + * The unerlying google-cloud Datastore instance + */ + get ds(): Datastore { + if (this.__ds === undefined) { + throw new Error('Trying to access Datastore instance but none was provided.'); + } + return this.__ds; + } +} + +export const instances = { + __refs: new Map(), + /** + * Retrieve a previously saved gstore instance. + * + * @param id The instance id + */ + get(id: string): Gstore { + const instance = this.__refs.get(id); + if (!instance) { + throw new Error(`Could not find gstore instance with id "${id}"`); + } + return instance; + }, + /** + * Save a gstore instance. + * + * @param id A unique name for the gstore instance + * @param instance A gstore instance + */ + set(id: string, instance: Gstore): void { + this.__refs.set(id, instance); + }, +}; + +export type Entity = GstoreEntity; + +export type Model = GstoreModel; + +export type Schema = GstoreSchema; + +export { QUERIES_FORMATS } from './constants'; + +export { ValidateResponse } from './helpers/validation'; + +export default Gstore; diff --git a/src/model.ts b/src/model.ts new file mode 100755 index 0000000..5290973 --- /dev/null +++ b/src/model.ts @@ -0,0 +1,1204 @@ +import is from 'is'; +import arrify from 'arrify'; +import extend from 'extend'; +import hooks from 'promised-hooks'; +import dsAdapterFactory from 'nsql-cache-datastore'; +import get from 'lodash.get'; +import set from 'lodash.set'; + +import { Transaction } from '@google-cloud/datastore'; + +import Gstore from './index'; +import Schema, { JoiConfig } from './schema'; +import Entity, { EntityResponse } from './entity'; +import Query, { QueryResponse, GstoreQuery } from './query'; +import { GstoreError, ERROR_CODES } from './errors'; +import helpers from './helpers'; +import { + FuncReturningPromise, + CustomEntityFunction, + IdType, + Ancestor, + EntityKey, + EntityData, + PopulateRef, + PopulateMetaForEntity, + PopulateFunction, + PromiseWithPopulate, + GenericObject, + JSONFormatType, + EntityFormatType, +} from './types'; + +const dsAdapter = dsAdapterFactory(); +const { populateHelpers } = helpers; + +const { keyToString } = dsAdapter; +const { populateFactory } = populateHelpers; + +export interface Model< + T extends object = GenericObject, + M extends object = { [key: string]: CustomEntityFunction } +> { + new (data: EntityData, id?: IdType, ancestors?: Ancestor, namespace?: string, key?: EntityKey): EntityResponse & + M; + + /** + * The gstore instance + */ + gstore: Gstore; + + /** + * The Model Schema + */ + schema: Schema; + + /** + * The Model Datastore entity kind + */ + entityKind: string; + + __hooksEnabled: boolean; + + /** + * Generates one or several entity key(s) for the Model. + * + * @param {(string | number)} id Entity id or name + * @param {(Array)} [ancestors] The entity Ancestors + * @param {string} [namespace] The entity Namespace + * @returns {entity.Key} + * @link https://sebloix.gitbook.io/gstore-node/model/methods/key + */ + key( + id: U, + ancestors?: Array, + namespace?: string, + ): U extends Array ? EntityKey[] : EntityKey; + + /** + * Fetch an Entity from the Datastore by _key_. + * + * @param {(string | number | string[] | number[])} id The entity ID + * @param {(Array)} [ancestors] The entity Ancestors + * @param {string} [namespace] The entity Namespace + * @param {*} [transaction] The current Datastore Transaction (if any) + * @param [options] Additional configuration + * @returns {Promise} The entity fetched from the Datastore + * @link https://sebloix.gitbook.io/gstore-node/model/methods/get + */ + get>( + id: U, + ancestors?: Array, + namespace?: string, + transaction?: Transaction, + options?: GetOptions, + ): PromiseWithPopulate ? EntityResponse[] : EntityResponse>; + + /** + * Update an Entity in the Datastore. This method _partially_ updates an entity data in the Datastore + * by doing a get() + merge the data + save() inside a Transaction. Unless you set `replace: true` in the parameter options. + * + * @param {(string | number)} id Entity id or name + * @param {*} data The data to update (it will be merged with the data in the Datastore + * unless options.replace is set to "true") + * @param {(Array)} [ancestors] The entity Ancestors + * @param {string} [namespace] The entity Namespace + * @param {*} [transaction] The current transaction (if any) + * @param {{ dataloader?: any, replace?: boolean }} [options] Additional configuration + * @returns {Promise} The entity updated in the Datastore + * @link https://sebloix.gitbook.io/gstore-node/model/methods/update + */ + update( + id: IdType, + data: EntityData, + ancestors?: Ancestor, + namespace?: string, + transaction?: Transaction, + options?: GenericObject, + ): Promise>; + + /** + * Delete an Entity from the Datastore + * + * @param {(string | number)} id Entity id or name + * @param {(Array)} [ancestors] The entity Ancestors + * @param {string} [namespace] The entity Namespace + * @param {*} [transaction] The current transaction (if any) + * @param {(entity.Key | entity.Key[])} [keys] If you already know the Key, you can provide it instead of passing + * an id/ancestors/namespace. You might then as well just call "gstore.ds.delete(Key)", + * but then you would not have the "hooks" triggered in case you have added some in your Schema. + * @returns {Promise<{ success: boolean, key: entity.Key, apiResponse: any }>} + * @link https://sebloix.gitbook.io/gstore-node/model/methods/delete + */ + delete( + id?: IdType | IdType[], + ancestors?: Ancestor, + namespace?: string, + transaction?: Transaction, + key?: EntityKey | EntityKey[], + options?: DeleteOptions, + ): Promise; + + /** + * Delete all the entities of a Model. + * It runs a query to fetch the entities by batches of 500 (limit set by the Datastore) and delete them. + * It then repeat the operation until no more entities are found. + * + * @static + * @param ancestors Optional Ancestors to add to the Query + * @param namespace Optional Namespace to run the Query into + * @link https://sebloix.gitbook.io/gstore-node/queries/deleteall + */ + deleteAll(ancestors?: Ancestor, namespace?: string): Promise; + + /** + * Clear all the Queries from the cache *linked* to the Model Entity Kind. + * One or multiple keys can also be passed to delete them from the cache. We normally don't have to call this method + * as gstore-node does it automatically each time an entity is added/edited or deleted. + * + * @param {(entity.Key | entity.Key[])} [keys] Optional entity Keys to remove from the cache with the Queries + * @returns {Promise} + * @link https://sebloix.gitbook.io/gstore-node/model/methods/clearcache + */ + clearCache(keys?: EntityKey | EntityKey[]): Promise<{ success: boolean }>; + + /** + * Dynamically remove a property from indexes. If you have set `explicityOnly: false` in your Schema options, + * then all the properties not declared in the Schema will be included in the indexes. + * This method allows you to dynamically exclude from indexes certain properties. + * + * @param {(string | string[])} propName Property name (can be one or an Array of properties) + * @link https://sebloix.gitbook.io/gstore-node/model/methods/exclude-from-indexes + */ + excludeFromIndexes(propName: string | string[]): void; + + /** + * Sanitize the entity data. It will remove all the properties marked as "write: false" on the schema. + * It will also convert "null" (string) to `null` value. + * + * @param {*} data The entity data to sanitize + * @link https://sebloix.gitbook.io/gstore-node/model/methods/sanitize + */ + sanitize(data: { [propName: string]: any }, options: { disabled: string[] }): EntityData; + + /** + * Initialize a Datastore Query for the Model's entity kind. + * + * @param {String} namespace Namespace for the Query + * @param {Object} transaction The transactioh to execute the query in (optional) + * + * @returns {Object} The Datastore query object. + * @link https://sebloix.gitbook.io/gstore-node/queries/google-cloud-queries + */ + query< + F extends JSONFormatType | EntityFormatType = JSONFormatType, + R = F extends EntityFormatType ? QueryResponse[]> : QueryResponse[]> + >( + namespace?: string, + transaction?: Transaction, + ): GstoreQuery; + + /** + * Shortcut for listing entities from a Model. List queries are meant to quickly list entities + * with predefined settings without having to manually create a query. + * + * @param {QueryListOptions} [options] + * @returns {Promise} + * @link https://sebloix.gitbook.io/gstore-node/queries/list + */ + list: Query['list']; + + /** + * Quickly find an entity by passing key/value pairs. + * + * @param keyValues Key / Values pairs + * @param ancestors Optional Ancestors to add to the Query + * @param namespace Optional Namespace to run the Query into + * @param options Additional configuration. + * @example + ``` + UserModel.findOne({ email: 'john[at]snow.com' }).then(...); + ``` + * @link https://sebloix.gitbook.io/gstore-node/queries/findone + */ + findOne: Query['findOne']; + + /** + * Find entities before or after an entity based on a property and a value. + * + * @param {string} propName The property to look around + * @param {*} value The property value + * @param {({ before: number, readAll?:boolean, format?: 'JSON' | 'ENTITY', showKey?: boolean } | { after: number, readAll?:boolean, format?: 'JSON' | 'ENTITY', showKey?: boolean } & QueryOptions)} options Additional configuration + * @returns {Promise} + * @example + ``` + // Find the next 20 post after March 1st 2019 + BlogPost.findAround('publishedOn', '2019-03-01', { after: 20 }) + ``` + * @link https://sebloix.gitbook.io/gstore-node/queries/findaround + */ + findAround: Query['findAround']; + + __compile(kind: string, schema: Schema): Model; + + __fetchEntityByKey(key: EntityKey, transaction?: Transaction, dataloader?: any, options?: GetOptions): Promise; + + __hasCache(options: { cache?: any }, type?: string): boolean; + + __populate(refs?: PopulateRef[][], options?: PopulateOptions): PopulateFunction; + + __hooksTransaction(transaction: Transaction, postHooks: FuncReturningPromise[]): void; + + __scopeHook(hook: string, args: GenericObject, hookName: string, hookType: 'pre' | 'post'): any; +} + +/** + * To improve performance and avoid looping over and over the entityData or Schema config + * we generate a meta object to cache useful data used later in models and entities methods. + */ +const extractMetaFromSchema = (schema: Schema): GenericObject => { + const meta: GenericObject = {}; + + Object.keys(schema.paths).forEach(k => { + switch (schema.paths[k as keyof T].type) { + case 'geoPoint': + // This allows us to automatically convert valid lng/lat objects + // to Datastore.geoPoints + meta.geoPointsProps = meta.geoPointsProps || []; + meta.geoPointsProps.push(k); + break; + case 'entityKey': + meta.refProps = meta.refProps || {}; + meta.refProps[k] = true; + break; + default: + } + }); + + return meta; +}; + +/** + * Pass all the "pre" and "post" hooks from schema to + * the current ModelInstance + */ +const registerHooksFromSchema = (model: Model, schema: Schema): void => { + const callQueue = schema.__callQueue.model; + + if (!Object.keys(callQueue).length) { + return; + } + + Object.keys(callQueue).forEach((method: string) => { + // Add Pre hooks + callQueue[method].pres.forEach(fn => { + (model as any).pre(method, fn); + }); + + // Add Post hooks + callQueue[method].post.forEach(fn => { + (model as any).post(method, fn); + }); + }); +}; + +/** + * Dynamically generate a new Gstore Model + * + * @param kind The Entity Kind + * @param schema The Gstore Schema + * @param gstore The Gstore instance + */ +export const generateModel = ( + kind: string, + schema: Schema, + gstore: Gstore, +): Model => { + if (!schema.__meta || Object.keys(schema.__meta).length === 0) { + schema.__meta = extractMetaFromSchema(schema); + } + const model: Model = class GstoreModel extends Entity { + static gstore: Gstore = gstore; + + static schema: Schema = schema; + + static entityKind: string = kind; + + static __hooksEnabled = true; + + static key ? EntityKey[] : EntityKey>( + ids: U, + ancestors?: Ancestor, + namespace?: string, + ): R { + const keys: EntityKey[] = []; + + let isMultiple = false; + + const getPath = (id?: IdType | null): IdType[] => { + let path: IdType[] = [this.entityKind]; + + if (typeof id !== 'undefined' && id !== null) { + path.push(id); + } + + if (ancestors && is.array(ancestors)) { + path = ancestors.concat(path); + } + + return path; + }; + + const getKey = (id?: IdType | null): EntityKey => { + const path = getPath(id); + let key; + + if (typeof namespace !== 'undefined' && namespace !== null) { + key = this.gstore.ds.key({ + namespace, + path, + }); + } else { + key = this.gstore.ds.key(path); + } + return key; + }; + + if (typeof ids !== 'undefined' && ids !== null) { + const idsArray = arrify(ids); + + isMultiple = idsArray.length > 1; + + idsArray.forEach(id => { + const key = getKey(id); + keys.push(key); + }); + } else { + const key = getKey(null); + keys.push(key); + } + + return isMultiple ? ((keys as unknown) as R) : ((keys[0] as unknown) as R); + } + + static get>( + id: U, + ancestors?: Ancestor, + namespace?: string, + transaction?: Transaction, + options: GetOptions = {}, + ): PromiseWithPopulate ? Entity[] : Entity> { + const ids = arrify(id); + + const key = this.key(ids, ancestors, namespace); + const refsToPopulate: PopulateRef[][] = []; + const { dataloader } = options; + + const onEntity = ( + entityDataFetched: EntityData | EntityData[], + ): Entity | null | Array | null> => { + const entityData = arrify(entityDataFetched); + + if ( + ids.length === 1 && + (entityData.length === 0 || typeof entityData[0] === 'undefined' || entityData[0] === null) + ) { + if (this.gstore.config.errorOnEntityNotFound) { + throw new GstoreError( + ERROR_CODES.ERR_ENTITY_NOT_FOUND, + `${this.entityKind} { ${ids[0].toString()} } not found`, + ); + } + + return null; + } + + // Convert entityData to Entity instance + const entity = (entityData as EntityData[]).map(data => { + if (typeof data === 'undefined' || data === null) { + return null; + } + return new this(data, undefined, undefined, undefined, (data as any)[this.gstore.ds.KEY]); + }); + + // TODO: Check if this is still useful?? + if (Array.isArray(id) && options.preserveOrder && entity.every(e => typeof e !== 'undefined' && e !== null)) { + (entity as Entity[]).sort((a, b) => id.indexOf(a.entityKey.id) - id.indexOf(b.entityKey.id)); + } + + return Array.isArray(id) ? (entity as Entity[]) : entity[0]; + }; + + /** + * If gstore has been initialize with a cache we first fetch + * the key(s) from it. + * gstore-cache underneath will call the "fetchHandler" with only the keys that haven't + * been found. The final response is the merge of the cache result + the fetch. + */ + const promise = this.__fetchEntityByKey(key, transaction, dataloader, options) + .then(onEntity) + .then(this.__populate(refsToPopulate, { ...options, transaction })); + + (promise as any).populate = populateFactory(refsToPopulate, promise, this.schema); + + return promise as PromiseWithPopulate ? Entity[] : Entity>; + } + + static update( + id: IdType, + data: EntityData, + ancestors?: Ancestor, + namespace?: string, + transaction?: Transaction, + options?: GenericObject, + ): Promise> { + this.__hooksEnabled = true; + + let entityDataUpdated: Entity; + let internalTransaction = false; + + const key = this.key(id, ancestors, namespace); + const replace = options && options.replace === true; + + const getEntity = (): Promise<{ key: EntityKey; data: EntityData }> => { + return transaction!.get(key).then(([entityData]: [EntityData]) => { + if (typeof entityData === 'undefined') { + throw new GstoreError(ERROR_CODES.ERR_ENTITY_NOT_FOUND, `Entity { ${id.toString()} } to update not found`); + } + + extend(false, entityData, data); + + const result = { + key: (entityData as any)[this.gstore.ds.KEY as any] as EntityKey, + data: entityData, + }; + + return result; + }); + }; + + const saveEntity = (datastoreFormat: { key: EntityKey; data: EntityData }): Promise> => { + const { key: entityKey, data: entityData } = datastoreFormat; + const entity = new this(entityData, undefined, undefined, undefined, entityKey); + + /** + * If a DataLoader instance is passed in the options + * attach it to the entity so it is available in "pre" hooks + */ + if (options && options.dataloader) { + entity.dataloader = options.dataloader; + } + + return entity.save(transaction); + }; + + const onTransactionSuccess = (): Promise> => { + /** + * Make sure to delete the cache for this key + */ + if (this.__hasCache(options)) { + return this.clearCache(key) + .then(() => entityDataUpdated) + .catch(err => { + let msg = 'Error while clearing the cache after updating the entity.'; + msg += 'The entity has been updated successfully though. '; + msg += 'Both the cache error and the entity updated have been attached.'; + const cacheError = new Error(msg); + (cacheError as any).__entityUpdated = entityDataUpdated; + (cacheError as any).__cacheError = err; + throw cacheError; + }); + } + + return Promise.resolve(entityDataUpdated); + }; + + const onEntityUpdated = (entity: Entity): Promise> => { + entityDataUpdated = entity; + + if (options && options.dataloader) { + options.dataloader.clear(key); + } + + if (internalTransaction) { + // If we created the Transaction instance internally for the update, we commit it + // otherwise we leave the commit() call to the transaction creator + return transaction! + .commit() + .then(() => + transaction!.execPostHooks().catch((err: any) => { + (entityDataUpdated as any)[entityDataUpdated.gstore.ERR_HOOKS] = ( + (entityDataUpdated as any)[entityDataUpdated.gstore.ERR_HOOKS] || [] + ).push(err); + }), + ) + .then(onTransactionSuccess); + } + + return onTransactionSuccess(); + }; + + const getAndUpdate = (): Promise> => + getEntity() + .then(saveEntity) + .then(onEntityUpdated); + + const onUpdateError = (err: Error | Error[]): Promise => { + const error = Array.isArray(err) ? err[0] : err; + if (internalTransaction) { + // If we created the Transaction instance internally for the update, we rollback it + // otherwise we leave the rollback() call to the transaction creator + + // TODO: Check why transaction!.rollback does not return a Promise by default + return (transaction!.rollback as any)().then(() => { + throw error; + }); + } + + throw error; + }; + + /** + * If options.replace is set to true we don't fetch the entity + * and save the data directly to the specified key, overriding any previous data. + */ + if (replace) { + return saveEntity({ key, data }) + .then(onEntityUpdated) + .catch(onUpdateError); + } + + if (typeof transaction === 'undefined' || transaction === null) { + internalTransaction = true; + transaction = this.gstore.ds.transaction(); + return transaction + .run() + .then(getAndUpdate) + .catch(onUpdateError); + } + + if (transaction.constructor.name !== 'Transaction') { + return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); + } + + return getAndUpdate(); + } + + static delete( + id?: IdType | IdType[], + ancestors?: Ancestor, + namespace?: string, + transaction?: Transaction, + key?: EntityKey | EntityKey[], + options: DeleteOptions = {}, + ): Promise { + this.__hooksEnabled = true; + + if (!key) { + key = this.key(id!, ancestors, namespace); + } + + if (transaction && transaction.constructor.name !== 'Transaction') { + return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); + } + + /** + * If it is a transaction, we create a hooks.post array that will be executed + * when transaction succeeds by calling transaction.execPostHooks() ---> returns a Promise + */ + if (transaction) { + // disable (post) hooks, to only trigger them if transaction succeeds + this.__hooksEnabled = false; + this.__hooksTransaction(transaction, (this as any).__posts ? (this as any).__posts.delete : undefined); + transaction.delete(key); + return Promise.resolve({ key }); + } + + return ((this.gstore.ds.delete(key) as unknown) as Promise).then((results?: [{ indexUpdates?: number }]) => { + const response: DeleteResponse = results ? results[0] : {}; + response.key = key; + + /** + * If we passed a DataLoader instance, we clear its cache + */ + if (options.dataloader) { + options.dataloader.clear(key); + } + + if (response.indexUpdates !== undefined) { + response.success = response.indexUpdates > 0; + } + + /** + * Make sure to delete the cache for this key + */ + if (this.__hasCache(options)) { + return this.clearCache(key!, options.clearQueries) + .then(() => response) + .catch(err => { + let msg = 'Error while clearing the cache after deleting the entity.'; + msg += 'The entity has been deleted successfully though. '; + msg += 'The cache error has been attached.'; + const cacheError = new Error(msg); + (cacheError as any).__response = response; + (cacheError as any).__cacheError = err; + throw cacheError; + }); + } + + return response; + }); + } + + static deleteAll(ancestors?: Ancestor, namespace?: string): Promise { + const maxEntitiesPerBatch = 500; + const timeoutBetweenBatches = 500; + + /** + * We limit the number of entities fetched to 100.000 to avoid hang up the system when + * there are > 1 million of entities to delete + */ + const QUERY_LIMIT = 100000; + + let currentBatch: number; + let totalBatches: number; + let entities: EntityData[]; + + const runQueryAndDeleteEntities = (): Promise => { + const deleteEntities = (batch: number): Promise => { + const onEntitiesDeleted = (): Promise => { + currentBatch += 1; + + if (currentBatch < totalBatches) { + // Still more batches to process + return new Promise((resolve): void => { + setTimeout(resolve, timeoutBetweenBatches); + }).then(() => deleteEntities(currentBatch)); + } + + // Re-run the fetch Query in case there are still entities to delete + return runQueryAndDeleteEntities(); + }; + + const indexStart = batch * maxEntitiesPerBatch; + const indexEnd = indexStart + maxEntitiesPerBatch; + const entitiesToDelete = entities.slice(indexStart, indexEnd); + + if ((this as any).__pres && {}.hasOwnProperty.call((this as any).__pres, 'delete')) { + // We execute delete in serie (chaining Promises) --> so we call each possible pre & post hooks + return entitiesToDelete + .reduce( + (promise, entity) => + promise.then(() => + this.delete(undefined, undefined, undefined, undefined, entity[this.gstore.ds.KEY as any]), + ), + Promise.resolve(), + ) + .then(onEntitiesDeleted); + } + + const keys = entitiesToDelete.map(entity => entity[this.gstore.ds.KEY as any]); + + // We only need to clear the Queries from the cache once, + // so we do it on the first batch. + const clearQueries = currentBatch === 0; + return this.delete(undefined, undefined, undefined, undefined, keys, { clearQueries }).then( + onEntitiesDeleted, + ); + }; + + const onQueryResponse = (data: QueryResponse): Promise => { + ({ entities } = data); + + if (entities.length === 0) { + // No more Data in table + return Promise.resolve({ + success: true, + message: `All ${this.entityKind} deleted successfully.`, + }); + } + + currentBatch = 0; + + // We calculate the total batches we will need to process + // The Datastore does not allow more than 500 keys at once when deleting. + totalBatches = Math.ceil(entities.length / maxEntitiesPerBatch); + + return deleteEntities(currentBatch); + }; + + // We query only limit number in case of big table + // If we query with more than million data query will hang up + const query = this.query(namespace); + if (ancestors) { + query.hasAncestor(this.gstore.ds.key(ancestors.slice())); + } + query.select('__key__'); + query.limit(QUERY_LIMIT); + + return query.run({ cache: false }).then(onQueryResponse); + }; + + return runQueryAndDeleteEntities(); + } + + static clearCache(keys: EntityKey | EntityKey[], clearQueries = true): Promise<{ success: boolean }> { + const handlers = []; + + if (clearQueries) { + handlers.push( + this.gstore.cache!.queries.clearQueriesByKind(this.entityKind).catch(e => { + if (e.code === 'ERR_NO_REDIS') { + // Silently fail if no Redis Client + return; + } + throw e; + }), + ); + } + + if (keys) { + const keysArray = arrify(keys); + handlers.push(this.gstore.cache!.keys.del(...keysArray)); + } + + return Promise.all(handlers).then(() => ({ success: true })); + } + + static excludeFromIndexes(properties: string | string[]): void { + properties = arrify(properties); + + properties.forEach(prop => { + if (!{}.hasOwnProperty.call(this.schema.paths, prop)) { + this.schema.path(prop, { optional: true, excludeFromIndexes: true }); + } else { + this.schema.paths[prop as keyof T].excludeFromIndexes = true; + } + }); + } + + static sanitize( + data: GenericObject, + options: { disabled: string[] } = { disabled: [] }, + ): { [P in keyof T]: T[P] } | GenericObject { + const key = data[this.gstore.ds.KEY as any]; // save the Key + + if (!is.object(data)) { + return {}; + } + + const isJoiSchema = schema.isJoi; + + let sanitized: GenericObject | undefined; + let joiOptions: JoiConfig['options']; + if (isJoiSchema) { + const { error, value } = schema.validateJoi(data); + if (!error) { + sanitized = { ...value }; + } + joiOptions = (schema.options.joi as JoiConfig).options || {}; + } + if (sanitized === undefined) { + sanitized = { ...data }; + } + + const isSchemaExplicitOnly = isJoiSchema ? joiOptions!.stripUnknown : schema.options.explicitOnly === true; + + const isWriteDisabled = options.disabled.includes('write'); + const hasSchemaRefProps = Boolean(schema.__meta.refProps); + let schemaHasProperty; + let isPropWritable; + let propValue; + + Object.keys(data).forEach(k => { + schemaHasProperty = {}.hasOwnProperty.call(schema.paths, k); + isPropWritable = schemaHasProperty ? schema.paths[k as keyof T].write !== false : true; + propValue = sanitized![k]; + + if ((isSchemaExplicitOnly && !schemaHasProperty) || (!isPropWritable && !isWriteDisabled)) { + delete sanitized![k]; + } else if (propValue === 'null') { + sanitized![k] = null; + } else if (hasSchemaRefProps && schema.__meta.refProps[k] && !this.gstore.ds.isKey(propValue)) { + // Replace populated entity by their entity Key + if (is.object(propValue) && propValue[this.gstore.ds.KEY]) { + sanitized![k] = propValue[this.gstore.ds.KEY]; + } + } + }); + + return key ? { ...sanitized, [this.gstore.ds.KEY]: key } : sanitized; + } + + // ------------------------------------ + // Private methods + // ------------------------------------ + + static __compile(newKind: string, newSchema: Schema): Model { + return generateModel(newKind, newSchema, gstore); + } + + static __fetchEntityByKey( + key: EntityKey | EntityKey[], + transaction?: Transaction, + dataloader?: any, + options?: GetOptions, + ): Promise | EntityData[]> { + const handler = (keys: EntityKey | EntityKey[]): Promise | EntityData[]> => { + const keysArray = arrify(keys); + if (transaction) { + if (transaction.constructor.name !== 'Transaction') { + return Promise.reject(new Error('Transaction needs to be a gcloud Transaction')); + } + return transaction.get(keysArray).then(([result]) => arrify(result)); + } + + if (dataloader) { + if (dataloader.constructor.name !== 'DataLoader') { + return Promise.reject( + new GstoreError(ERROR_CODES.ERR_GENERIC, 'dataloader must be a "DataLoader" instance'), + ); + } + return dataloader.loadMany(keysArray).then((result: EntityData) => arrify(result)); + } + return this.gstore.ds.get(keysArray).then(([result]: [any]) => arrify(result)); + }; + + if (this.__hasCache(options)) { + return this.gstore.cache!.keys.read( + // nsql-cache requires an array for multiple and a single key when *not* multiple + Array.isArray(key) && key.length === 1 ? key[0] : key, + options, + handler, + ); + } + return handler(key); + } + + // Helper to know if the cache is "on" when fetching entities + static __hasCache(options: { cache?: any } = {}, type = 'keys'): boolean { + if (typeof this.gstore.cache === 'undefined') { + return false; + } + if (typeof options.cache !== 'undefined') { + return options.cache; + } + if (this.gstore.cache.config.global === false) { + return false; + } + if (this.gstore.cache.config.ttl[type] === -1) { + return false; + } + return true; + } + + static __populate(refs?: PopulateRef[][], options: PopulateOptions = {}): PopulateFunction { + const dataloader = options.dataloader || this.gstore.createDataLoader(); + + const getPopulateMetaForEntity = ( + entity: Entity | EntityData, + entityRefs: PopulateRef[], + ): PopulateMetaForEntity => { + const keysToFetch: EntityKey[] = []; + const mapKeyToPropAndSelect: { [key: string]: { ref: PopulateRef } } = {}; + + const isEntityClass = entity instanceof Entity; + entityRefs.forEach(ref => { + const { path } = ref; + const entityData: EntityData = isEntityClass ? entity.entityData : entity; + + const key = get(entityData, path); + + if (!key) { + set(entityData, path, null); + return; + } + + if (!this.gstore.ds.isKey(key)) { + throw new Error(`[gstore] ${path} is not a Datastore Key. Reference entity can't be fetched.`); + } + + // Stringify the key + const strKey = keyToString(key); + // Add it to our map + mapKeyToPropAndSelect[strKey] = { ref }; + // Add to our array to be fetched + keysToFetch.push(key); + }); + + return { entity, keysToFetch, mapKeyToPropAndSelect }; + }; + + const populateFn: PopulateFunction = entitiesToProcess => { + if (!refs || !refs.length || entitiesToProcess === null) { + // Nothing to do here... + return Promise.resolve(entitiesToProcess); + } + + // Keep track if we provided an array for the response format + const isArray = Array.isArray(entitiesToProcess); + const entities = arrify(entitiesToProcess); + const isEntityClass = entities[0] instanceof Entity; + + // Fetches the entity references at the current + // object tree depth + const fetchRefsEntitiesRefsAtLevel = (entityRefs: PopulateRef[]): Promise => { + // For each one of the entities to process, we gatter some meta data + // like the keys to fetch for that entity in order to populate its refs. + // Dataloaader will take care to only fetch unique keys on the Datastore + const meta = (entities as Entity[]).map(entity => getPopulateMetaForEntity(entity, entityRefs)); + + const onKeysFetched = ( + response: EntityData[] | null, + { entity, keysToFetch, mapKeyToPropAndSelect }: PopulateMetaForEntity, + ): void => { + if (!response) { + // No keys have been fetched + return; + } + + const entityData = isEntityClass ? { ...entity.entityData } : entity; + + const mergeRefEntitiesToEntityData = (data: EntityData, i: number): void => { + const key = keysToFetch[i]; + const strKey = keyToString(key); + const { + ref: { path, select }, + } = mapKeyToPropAndSelect[strKey]; + + if (!data) { + set(entityData, path, data); + return; + } + + const EmbeddedModel = this.gstore.model(key.kind); + const embeddedEntity = new EmbeddedModel(data, undefined, undefined, undefined, key); + + // prettier-ignore + // If "select" fields are provided, we return them, + // otherwise we return the entity plain() json + const json = + select.length && !select.some(s => s === '*') + ? select.reduce( + (acc, field) => { + acc = { + ...acc, + [field]: data[field] || null, + }; + return acc; + }, + {} as { [key: string]: any } + ) + : embeddedEntity.plain(); + + set(entityData, path, { ...json, id: key.name || key.id }); + + if (isEntityClass) { + entity.entityData = entityData; + } + }; + + // Loop over all dataloader.loadMany() responses + response.forEach(mergeRefEntitiesToEntityData); + }; + + const promises = meta.map(({ keysToFetch }) => + keysToFetch.length + ? (this.__fetchEntityByKey(keysToFetch, options.transaction, dataloader, options) as Promise< + EntityData[] + >) + : Promise.resolve(null), + ); + + return Promise.all(promises).then(result => { + // Loop over all responses from dataloader.loadMany() calls + result.forEach((res, i) => onKeysFetched(res, meta[i])); + }); + }; + + return new Promise((resolve, reject): void => { + // At each tree level we fetch the entity references in series. + refs + .reduce( + (chainedPromise, entityRefs) => chainedPromise.then(() => fetchRefsEntitiesRefsAtLevel(entityRefs)), + Promise.resolve(), + ) + .then(() => { + resolve(isArray ? entities : entities[0]); + }) + .catch(reject); + }); + }; + + return populateFn; + } + + // Add "post" hooks to a transaction + static __hooksTransaction(transaction: Transaction, postHooks: FuncReturningPromise[]): void { + const _this = this; // eslint-disable-line @typescript-eslint/no-this-alias + postHooks = arrify(postHooks); + + if (!{}.hasOwnProperty.call(transaction, 'hooks')) { + transaction.hooks = { + post: [], + }; + } + + transaction.hooks.post = [...transaction.hooks.post, ...postHooks]; + + transaction.execPostHooks = function executePostHooks(): Promise { + if (!this.hooks.post) { + return Promise.resolve(); + } + return (this.hooks.post as FuncReturningPromise[]).reduce( + (promise, hook) => promise.then(hook.bind(_this)), + Promise.resolve() as Promise, + ); + }; + } + + // Helper to change the function scope (the "this" value) for a hook if necessary + // TODO: Refactor this in promised-hook to make this behaviour more declarative. + static __scopeHook(hook: string, args: GenericObject, hookName: string, hookType: 'pre' | 'post'): any { + /** + * For "delete" hooks we want to set the scope to + * the entity instance we are going to delete + * We won't have any entity data inside the entity but, if needed, + * we can then call the "datastoreEntity()" helper on the scope (this) + * from inside the hook. + * For "multiple" ids to delete, we obviously can't set any scope. + */ + const getScopeForDeleteHooks = (): any => { + const id = + is.object(args[0]) && {}.hasOwnProperty.call(args[0], '__override') ? arrify(args[0].__override)[0] : args[0]; + + if (is.array(id)) { + return null; + } + + let ancestors; + let namespace; + let key; + + if (hookType === 'post') { + ({ key } = args); + if (is.array(key)) { + return null; + } + } else { + ({ 1: ancestors, 2: namespace, 4: key } = args); + } + + if (!id && !ancestors && !namespace && !key) { + return undefined; + } + + return new this({} as EntityData, id, ancestors, namespace, key); + }; + + switch (hook) { + case 'delete': + return getScopeForDeleteHooks(); + default: + return this; + } + } + + // ----------------------------------------------------------- + // Other properties and methods attached to the Model Class + // ----------------------------------------------------------- + + static pre: any; // Is added below when wrapping with hooks + + static post: any; // Is added below when wrapping with hooks + + static query: any; // Is added below from the Query instance + + static findOne: any; // Is added below from the Query instance + + static list: any; // Is added below from the Query instance + + static findAround: any; // Is added below from the Query instance + } as Model & T; + + const query = new Query(model); + const { initQuery, list, findOne, findAround } = query; + + model.query = initQuery.bind(query); + model.list = list.bind(query); + model.findOne = findOne.bind(query); + model.findAround = findAround.bind(query); + + // TODO: Refactor how the Model/Entity relationship! + // Attach props to prototype + model.prototype.__gstore = gstore; + model.prototype.__schema = schema; + model.prototype.__entityKind = kind; + + // Wrap the Model to add "pre" and "post" hooks functionalities + hooks.wrap(model); + registerHooksFromSchema(model, schema); + + return model; +}; + +interface GetOptions { + /** + * If you have provided an Array of ids, the order returned by the Datastore is not guaranteed. + * If you need the entities back in the same order of the IDs provided, then set `preserveOrder: true` + * + * @type {boolean} + * @default false + */ + preserveOrder?: boolean; + /** + * An optional Dataloader instance. Read more about Dataloader in the docs. + * + * @link https://sebloix.gitbook.io/gstore-node/cache-dataloader/dataloader + */ + dataloader?: any; + /** + * Only if the cache has been turned "on" when initializing gstore. + * Fetch the entity from the cache first. If you want to bypass the cache + * and fetch the entiy from the Datastore, set `cache: false`. + * + * @type {boolean} + * @default The "global" cache configuration + * @link https://sebloix.gitbook.io/gstore-node/cache-dataloader/cache + */ + cache?: boolean; + /** + * Only if the cache has been turned "on" when initializing gstore. + * After the entty has been fetched from the Datastore it will be added to the cache. + * You can specify here a custom ttl (Time To Live) for the cache of the entity. + * + * @type {(number | { [propName: string] : number })} + * @default The "ttl.keys" cache configuration + * @link https://sebloix.gitbook.io/gstore-node/cache-dataloader/cache + */ + ttl?: number | { [propName: string]: number }; +} + +interface DeleteOptions { + dataloader?: any; + cache?: any; + clearQueries?: boolean; +} + +interface DeleteResponse { + key?: EntityKey | EntityKey[]; + success?: boolean; + apiResponse?: any; + indexUpdates?: number; +} + +interface DeleteAllResponse { + success: boolean; + message: string; +} + +interface PopulateOptions extends GetOptions { + transaction?: Transaction; +} + +export default Model; diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 0000000..294ac58 --- /dev/null +++ b/src/query.ts @@ -0,0 +1,332 @@ +import extend from 'extend'; +import is from 'is'; + +import { Transaction, Query as DatastoreQuery } from '@google-cloud/datastore'; + +import Model from './model'; +import { EntityResponse } from './entity'; +import helpers from './helpers'; +import { GstoreError, ERROR_CODES } from './errors'; +import { datastoreSerializer } from './serializers'; +import { + PopulateRef, + EntityData, + EntityFormatType, + JSONFormatType, + PromiseWithPopulate, + DatastoreOperator, + OrderOptions, +} from './types'; + +const { queryHelpers, populateHelpers } = helpers; +const { populateFactory } = populateHelpers; +const { createDatastoreQueryForModel, buildQueryFromOptions } = queryHelpers; + +class Query { + public Model: Model; + + constructor(model: Model) { + this.Model = model; + } + + initQuery>(namespace?: string, transaction?: Transaction): GstoreQuery { + const query: DatastoreQuery = createDatastoreQueryForModel(this.Model, namespace, transaction); + + const enhancedQueryRun = ( + options?: QueryOptions, + responseHandler = (res: QueryResponse): QueryResponse => res, + ): PromiseWithPopulate => { + options = extend(true, {}, this.Model.schema.options.queries, options); + + /** + * Array to keep all the references entities to fetch + */ + const refsToPopulate: PopulateRef[][] = []; + let promise; + + const onResponse = (data: [EntityData[], { moreResults: string; endCursor: string }]): QueryResponse => { + let entities = data[0]; + const info = data[1]; + + // Convert to JSON or ENTITY acording to which format is passed. (default = JSON) + // If JSON => Add id property to entities and suppress properties with "read" config is set to `false` + entities = entities.map(entity => datastoreSerializer.fromDatastore(entity, this.Model, options)); + + const response: QueryResponse = { + entities, + }; + + if (info.moreResults !== this.Model.gstore.ds.NO_MORE_RESULTS) { + response.nextPageCursor = info.endCursor; + } + + return response; + }; + + // prettier-ignore + const populateHandler = (response: QueryResponse): QueryResponse | Promise> => + refsToPopulate.length + ? this.Model.__populate(refsToPopulate, options)(response.entities).then((entitiesPopulated: any) => ({ + ...response, + entities: entitiesPopulated as EntityData[], + })) + : response; + + if (this.Model.__hasCache(options, 'queries')) { + promise = this.Model.gstore + .cache!.queries.read(query, options, (query as any).__originalRun.bind(query)) + .then(onResponse) + .then(populateHandler) + .then(responseHandler); + } else { + promise = (query as any).__originalRun + .call(query, options) + .then(onResponse) + .then(populateHandler) + .then(responseHandler); + } + + promise.populate = populateFactory(refsToPopulate, promise, this.Model.schema); + return promise; + }; + + /* eslint-disable @typescript-eslint/unbound-method */ + // ((query as unknown) as GstoreQuery>).__originalRun = ((query as unknown) as DatastoreQuery).run; + ((query as unknown) as GstoreQuery).__originalRun = query.run; + (query as any).run = enhancedQueryRun; + /* eslint-enable @typescript-eslint/unbound-method */ + + return (query as unknown) as GstoreQuery; + } + + list< + U extends QueryListOptions, + Outputformat = U['format'] extends EntityFormatType ? EntityResponse : EntityData + >(options: U = {} as U): PromiseWithPopulate> { + // If global options set in schema, we extend it with passed options + if ({}.hasOwnProperty.call(this.Model.schema.shortcutQueries, 'list')) { + options = extend({}, this.Model.schema.shortcutQueries.list, options); + } + + let query = this.initQuery>(options && options.namespace); + + // Build Datastore Query from options passed + query = buildQueryFromOptions>(query, options, this.Model.gstore.ds); + + const { limit, offset, order, select, ancestors, filters, start, ...rest } = options; + return query.run(rest); + } + + findOne( + keyValues: { [P in keyof Partial]: T[P] }, + ancestors?: Array, + namespace?: string, + options?: { + cache?: boolean; + ttl?: number | { [key: string]: number }; + }, + ): PromiseWithPopulate | null> { + this.Model.__hooksEnabled = true; + + if (!is.object(keyValues)) { + return Promise.reject(new Error('[gstore.findOne()]: "Params" has to be an object.')) as PromiseWithPopulate< + never + >; + } + + const query = this.initQuery | null>(namespace); + query.limit(1); + + Object.keys(keyValues).forEach(k => { + query.filter(k as keyof T, keyValues[k as keyof T]); + }); + + if (ancestors) { + query.hasAncestor(this.Model.gstore.ds.key(ancestors.slice())); + } + + const responseHandler = ({ entities }: QueryResponse): EntityResponse | null => { + if (entities.length === 0) { + if (this.Model.gstore.config.errorOnEntityNotFound) { + throw new GstoreError(ERROR_CODES.ERR_ENTITY_NOT_FOUND, `${this.Model.entityKind} not found`); + } + return null; + } + + const [e] = entities; + const entity = new this.Model(e, undefined, undefined, undefined, (e as any)[this.Model.gstore.ds.KEY]); + return entity; + }; + return query.run(options, responseHandler); + } + + /** + * Find entities before or after an entity based on a property and a value. + * + * @static + * @param {string} propName The property to look around + * @param {*} value The property value + * @param options Additional configuration + * @returns {Promise} + * @example + ``` + // Find the next 20 post after March 1st 2018 + BlogPost.findAround('publishedOn', '2018-03-01', { after: 20 }) + ``` + * @link https://sebloix.gitbook.io/gstore-node/queries/findaround + */ + findAround< + U extends QueryFindAroundOptions, + Outputformat = U['format'] extends EntityFormatType ? EntityResponse : EntityData + >(property: keyof T, value: any, options: U, namespace?: string): PromiseWithPopulate { + const validateArguments = (): { error: Error | null } => { + if (!property || !value || !options) { + return { error: new Error('[gstore.findAround()]: Not all the arguments were provided.') }; + } + + if (!is.object(options)) { + return { error: new Error('[gstore.findAround()]: Options pased has to be an object.') }; + } + + if (!{}.hasOwnProperty.call(options, 'after') && !{}.hasOwnProperty.call(options, 'before')) { + return { error: new Error('[gstore.findAround()]: You must set "after" or "before" in options.') }; + } + + if ({}.hasOwnProperty.call(options, 'after') && {}.hasOwnProperty.call(options, 'before')) { + return { error: new Error('[gstore.findAround()]: You can\'t set both "after" and "before".') }; + } + + return { error: null }; + }; + + const { error } = validateArguments(); + + if (error) { + return Promise.reject(error) as PromiseWithPopulate; + } + + const query = this.initQuery(namespace); + const op = options.after ? '>' : '<'; + const descending = !!options.after; + + query.filter(property, op, value); + query.order(property, { descending }); + query.limit(options.after ? options.after : options.before!); + + const { after, before, ...rest } = options; + return query.run(rest, (res: QueryResponse) => (res.entities as unknown) as Outputformat[]); + } +} + +export interface GstoreQuery extends Omit { + __originalRun: DatastoreQuery['run']; + run: QueryRunFunc; + filter

(property: P, value: T[P]): DatastoreQuery; + filter

(property: P, operator: DatastoreOperator, value: T[P]): DatastoreQuery; + order(property: keyof T, options?: OrderOptions): this; +} + +type QueryRunFunc = ( + options?: QueryOptions, + responseHandler?: (res: QueryResponse) => R, +) => PromiseWithPopulate; + +export interface QueryOptions { + /** + * Specify either strong or eventual. If not specified, default values are chosen by Datastore for the operation. + * Learn more about strong and eventual consistency in the link below + * + * @type {('strong' | 'eventual')} + * @link https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore + */ + consistency?: 'strong' | 'eventual'; + /** + * If set to true will return all the properties of the entity, + * regardless of the *read* parameter defined in the Schema + * + * @type {boolean} + * @default false + */ + readAll?: boolean; + /** + * Response format for the entities. Either plain object or entity instances + * + * @type {'JSON' | 'ENTITY'} + * @default 'JSON' + */ + format?: JSONFormatType | EntityFormatType; + /** + * Add a "__key" property to the entity data with the complete Key from the Datastore. + * + * @type {boolean} + * @default false + */ + showKey?: boolean; + /** + * If set to true, it will read from the cache and prime the cache with the response of the query. + * + * @type {boolean} + * @default The "global" cache configuration. + */ + cache?: boolean; + /** + * Custom TTL value for the cache. For multi-store it can be an object of ttl values + * + * @type {(number | { [propName: string]: number })} + * @default The cache.ttl.queries value + */ + ttl?: number | { [propName: string]: number }; +} + +export interface QueryListOptions extends QueryOptions { + /** + * Optional namespace for the query. + */ + namespace?: string; + /** + * The total number of entities to return from the query. + */ + limit?: number; + /** + * Descending is optional and default to "false" + * + * @example ```{ property: 'userName', descending: true }``` + */ + order?: { property: keyof T; descending?: boolean } | { property: keyof T; descending?: boolean }[]; + /** + * Retrieve only select properties from the matched entities. + */ + select?: string | string[]; + /** + * Supported comparison operators are =, <, >, <=, and >=. + * "Not equal" and IN operators are currently not supported. + */ + filters?: [string, any] | [string, DatastoreOperator, any] | (any)[][]; + /** + * Filter a query by ancestors. + */ + ancestors?: Array; + /** + * Set a starting cursor to a query. + */ + start?: string; + /** + * Set an offset on a query. + */ + offset?: number; +} + +export interface QueryFindAroundOptions extends QueryOptions { + before?: number; + after?: number; + readAll?: boolean; + format?: JSONFormatType | EntityFormatType; + showKey?: boolean; +} + +export interface QueryResponse[]> { + entities: F; + nextPageCursor?: string; +} + +export default Query; diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..ee7a5dc --- /dev/null +++ b/src/schema.ts @@ -0,0 +1,394 @@ +import optional from 'optional'; +import extend from 'extend'; +import is from 'is'; + +import { QUERIES_FORMATS } from './constants'; +import VirtualType from './virtualType'; +import { ValidationError, ERROR_CODES } from './errors'; +import { + FunctionType, + FuncReturningPromise, + CustomEntityFunction, + GenericObject, + EntityFormatType, + JSONFormatType, +} from './types'; +import { QueryListOptions } from './query'; + +const Joi = optional('@hapi/joi') || optional('joi'); + +const IS_QUERY_METHOD: { [key: string]: boolean } = { + update: true, + delete: true, + findOne: true, +}; + +const DEFAULT_OPTIONS = { + validateBeforeSave: true, + explicitOnly: true, + excludeLargeProperties: false, + queries: { + readAll: false, + format: QUERIES_FORMATS.JSON, + }, +}; + +const RESERVED_PROPERTY_NAMES: { [key: string]: boolean } = { + _events: true, + _eventsCount: true, + _lazySetupHooks: true, + _maxListeners: true, + _posts: true, + _pres: true, + className: true, + constructor: true, + delete: true, + domain: true, + ds: true, + emit: true, + entityData: true, + entityKey: true, + errors: true, + excludeFromIndexes: true, + get: true, + getEntityDataWithVirtuals: true, + gstore: true, + hook: true, + init: true, + isModified: true, + isNew: true, + listeners: true, + model: true, + modelName: true, + on: true, + once: true, + plain: true, + post: true, + pre: true, + removeListener: true, + removePost: true, + removePre: true, + save: true, + schema: true, + set: true, + toObject: true, + update: true, + validate: true, +}; + +/** + * gstore Schema + */ +class Schema }> { + public readonly methods: { [P in keyof M]: CustomEntityFunction }; + + public readonly paths: { [P in keyof T]: SchemaPathDefinition }; + + public readonly options: SchemaOptions = {}; + + public __meta: GenericObject = {}; + + public readonly __virtuals: { [key: string]: VirtualType }; + + public __callQueue: { + model: { + [key: string]: { + pres: (FuncReturningPromise | FuncReturningPromise[])[]; + post: (FuncReturningPromise | FuncReturningPromise[])[]; + }; + }; + entity: { + [key: string]: { + pres: (FuncReturningPromise | FuncReturningPromise[])[]; + post: (FuncReturningPromise | FuncReturningPromise[])[]; + }; + }; + }; + + public readonly shortcutQueries: { [key: string]: QueryListOptions }; + + public joiSchema?: GenericObject; + + constructor(properties: { [P in keyof T]: SchemaPathDefinition }, options?: SchemaOptions) { + this.methods = {} as any; + this.__virtuals = {}; + this.shortcutQueries = {}; + this.paths = {} as { [P in keyof T]: SchemaPathDefinition }; + this.__callQueue = { + model: {}, + entity: {}, + }; + + this.options = Schema.initSchemaOptions(options); + + Object.entries(properties).forEach(([key, definition]) => { + if (RESERVED_PROPERTY_NAMES[key]) { + throw new Error(`${key} is reserved and can not be used as a schema pathname`); + } + + this.paths[key as keyof T] = definition as SchemaPathDefinition; + }); + + if (options) { + this.joiSchema = Schema.initJoiSchema(properties, this.options.joi); + } + } + + /** + * Add custom methods to entities. + * @link https://sebloix.gitbook.io/gstore-node/schema/custom-methods + * + * @example + * ``` + * schema.methods.profilePict = function() { + return this.model('Image').get(this.imgIdx) + * } + * ``` + */ + method(name: string | { [key: string]: FunctionType }, fn: FunctionType): void { + if (typeof name !== 'string') { + if (typeof name !== 'object') { + return; + } + Object.keys(name).forEach(k => { + if (typeof name[k] === 'function') { + this.methods[k as keyof M] = name[k]; + } + }); + } else if (typeof fn === 'function') { + this.methods[name as keyof M] = fn; + } + } + + queries(type: 'list', settings: QueryListOptions): void { + this.shortcutQueries[type] = settings; + } + + /** + * Getter / Setter for Schema paths. + * + * @param {string} propName The entity property + * @param {SchemaPathDefinition} [definition] The property definition + * @link https://sebloix.gitbook.io/gstore-node/schema/methods/path + */ + path(propName: string, definition: SchemaPathDefinition): Schema | SchemaPathDefinition | undefined { + if (typeof definition === 'undefined') { + if (this.paths[propName as keyof T]) { + return this.paths[propName as keyof T]; + } + return undefined; + } + + if (RESERVED_PROPERTY_NAMES[propName]) { + throw new Error(`${propName} is reserved and can not be used as a schema pathname`); + } + + this.paths[propName as keyof T] = definition; + return this; + } + + /** + * Register a middleware to be executed before "save()", "delete()", "findOne()" or any of your custom method. + * The callback will receive the original argument(s) passed to the target method. You can modify them + * in your resolve passing an object with an __override property containing the new parameter(s) + * for the target method. + * + * @param {string} method The target method to add the hook to + * @param {(...args: any[]) => Promise} fn Function to execute before the target method. + * It must return a Promise + * @link https://sebloix.gitbook.io/gstore-node/middleware-hooks/pre-hooks + */ + pre(method: string, fn: FuncReturningPromise | FuncReturningPromise[]): number { + const queue = IS_QUERY_METHOD[method] ? this.__callQueue.model : this.__callQueue.entity; + + if (!{}.hasOwnProperty.call(queue, method)) { + queue[method] = { + pres: [], + post: [], + }; + } + + return queue[method].pres.push(fn); + } + + /** + * Register a "post" middelware to execute after a target method. + * + * @param {string} method The target method to add the hook to + * @param {(response: any) => Promise} callback Function to execute after the target method. + * It must return a Promise + * @link https://sebloix.gitbook.io/gstore-node/middleware-hooks/post-hooks + */ + post(method: string, fn: FuncReturningPromise | FuncReturningPromise[]): number { + const queue = IS_QUERY_METHOD[method] ? this.__callQueue.model : this.__callQueue.entity; + + if (!{}.hasOwnProperty.call(queue, method)) { + queue[method] = { + pres: [], + post: [], + }; + } + + return queue[method].post.push(fn); + } + + /** + * Getter / Setter of a virtual property. + * Virtual properties are created dynamically and not saved in the Datastore. + * + * @param {string} propName The virtual property name + * @link https://sebloix.gitbook.io/gstore-node/schema/methods/virtual + */ + virtual(propName: string): VirtualType { + if (RESERVED_PROPERTY_NAMES[propName]) { + throw new Error(`${propName} is reserved and can not be used as virtual property.`); + } + if (!{}.hasOwnProperty.call(this.__virtuals, propName)) { + this.__virtuals[propName] = new VirtualType(propName); + } + return this.__virtuals[propName]; + } + + /** + * Executes joi.validate on given data. If the schema does not have a joi config object data is returned. + * + * @param {*} data The data to sanitize + * @returns {*} The data sanitized + */ + validateJoi(entityData: any): any { + if (!this.isJoi) { + return { + error: new ValidationError(ERROR_CODES.ERR_GENERIC, 'Schema does not have a joi configuration object'), + value: entityData, + }; + } + return this.joiSchema!.validate(entityData, (this.options.joi as JoiConfig).options || {}); + } + + /** + * Flag that returns "true" if the schema has a joi config object. + */ + get isJoi(): boolean { + return !is.undefined(this.joiSchema); + } + + static initSchemaOptions(provided?: SchemaOptions): SchemaOptions { + const options = extend(true, {}, DEFAULT_OPTIONS, provided); + + if (options.joi) { + const joiOptionsDefault = { + options: { + allowUnknown: options.explicitOnly !== true, + }, + }; + if (is.object(options.joi)) { + options.joi = extend(true, {}, joiOptionsDefault, options.joi); + } else { + options.joi = { ...joiOptionsDefault }; + } + if (!Object.prototype.hasOwnProperty.call((options.joi as JoiConfig).options, 'stripUnknown')) { + (options.joi as JoiConfig).options!.stripUnknown = (options.joi as JoiConfig).options!.allowUnknown !== true; + } + } + + return options; + } + + static initJoiSchema( + schema: { [key: string]: SchemaPathDefinition }, + joiConfig?: boolean | JoiConfig, + ): GenericObject | undefined { + if (!is.object(joiConfig)) { + return undefined; + } + + const hasExtra = is.object((joiConfig as JoiConfig).extra); + const joiKeys: { [key: string]: SchemaPathDefinition['joi'] } = {}; + + Object.entries(schema).forEach(([key, definition]) => { + if ({}.hasOwnProperty.call(definition, 'joi')) { + joiKeys[key] = definition.joi; + } + }); + + let joiSchema = Joi.object().keys(joiKeys); + let args; + + if (hasExtra) { + Object.keys((joiConfig as JoiConfig).extra!).forEach(k => { + if (is.function(joiSchema[k])) { + args = (joiConfig as JoiConfig).extra![k]; + joiSchema = joiSchema[k](...args); + } + }); + } + + return joiSchema; + } + + /** + * Custom Schema Types + */ + static Types = { + /** + * Datastore Double object. For long doubles, a string can be provided. + * @link https://googleapis.dev/nodejs/datastore/latest/Double.html + */ + Double: 'double', + /** + * Datastore Geo Point object. + * @link https://googleapis.dev/nodejs/datastore/latest/GeoPoint.html + */ + GeoPoint: 'geoPoint', + /** + * Used to reference another entity. See the `populate()` doc. + * @link https://sebloix.gitbook.io/gstore-node/populate + */ + Key: 'entityKey', + }; +} + +export interface SchemaPathDefinition { + type?: PropType; + validate?: Validator; + optional?: boolean; + default?: any; + excludeFromIndexes?: boolean | string | string[]; + read?: boolean; + excludeFromRead?: string[]; + write?: boolean; + required?: boolean; + values?: any[]; + joi?: any; + ref?: string; +} + +export type JoiConfig = { extra?: GenericObject; options?: GenericObject }; + +export interface SchemaOptions { + validateBeforeSave?: boolean; + explicitOnly?: boolean; + excludeLargeProperties?: boolean; + queries?: { + readAll?: boolean; + format?: JSONFormatType | EntityFormatType; + showKey?: string; + }; + joi?: boolean | JoiConfig; +} + +export type Validator = string | { rule: string | ((...args: any[]) => boolean); args: any[] }; + +export type PropType = + | NumberConstructor + | StringConstructor + | ObjectConstructor + | ArrayConstructor + | BooleanConstructor + | DateConstructor + | typeof Buffer + | 'double' + | 'geoPoint' + | 'entityKey'; + +export default Schema; diff --git a/src/serializers/datastore.ts b/src/serializers/datastore.ts new file mode 100644 index 0000000..9af2dd5 --- /dev/null +++ b/src/serializers/datastore.ts @@ -0,0 +1,162 @@ +import is from 'is'; +import arrify from 'arrify'; + +import Entity from '../entity'; +import GstoreModel from '../model'; +import { GenericObject, EntityKey, EntityData, IdType, DatastoreSaveMethod } from '../types'; + +type ToDatastoreOptions = { method?: DatastoreSaveMethod }; + +type DatastoreFormat = { + key: EntityKey; + data: EntityData; + excludeLargeProperties?: boolean; + excludeFromIndexes?: string[]; + method?: DatastoreSaveMethod; +}; + +const getExcludeFromIndexes = (data: GenericObject, entity: Entity): string[] => + Object.entries(data) + .filter(([, value]) => value !== null) + .map(([key]) => entity.__excludeFromIndexes[key as keyof T] as string[]) + .filter(v => v !== undefined) + .reduce((acc: string[], arr) => [...acc, ...arr], []); + +const idFromKey = (key: EntityKey): IdType => key.path[key.path.length - 1]; + +const toDatastore = ( + entity: Entity, + options: ToDatastoreOptions | undefined = {}, +): DatastoreFormat => { + const data = Object.entries(entity.entityData).reduce( + (acc, [key, value]) => { + if (typeof value !== 'undefined') { + acc[key] = value; + } + return acc; + }, + {} as { [key: string]: any }, + ); + + const excludeFromIndexes = getExcludeFromIndexes(data, entity); + + const datastoreFormat: DatastoreFormat = { + key: entity.entityKey, + data, + excludeLargeProperties: entity.schema.options.excludeLargeProperties, + }; + + if (excludeFromIndexes.length > 0) { + datastoreFormat.excludeFromIndexes = excludeFromIndexes; + } + + if (options.method) { + datastoreFormat.method = options.method; + } + + return datastoreFormat; +}; + +const fromDatastore = ( + entityData: EntityData, + Model: GstoreModel, + options: { format?: F; readAll?: boolean; showKey?: boolean } = {}, +): R => { + const convertToJson = (): GenericObject => { + options.readAll = typeof options.readAll === 'undefined' ? false : options.readAll; + + const { schema, gstore } = Model; + const { KEY } = gstore.ds; + const entityKey = entityData[KEY as any]; + const data: { [key: string]: any } = { + id: idFromKey(entityKey), + }; + data[KEY as any] = entityKey; + + Object.keys(entityData).forEach(k => { + if (options.readAll || !{}.hasOwnProperty.call(schema.paths, k) || schema.paths[k].read !== false) { + let value = entityData[k]; + + if ({}.hasOwnProperty.call(schema.paths, k)) { + // During queries @google-cloud converts datetime to number + if (schema.paths[k].type && (schema.paths[k].type! as Function).name === 'Date' && is.number(value)) { + value = new Date(value / 1000); + } + + // Sanitise embedded objects + if ( + typeof schema.paths[k].excludeFromRead !== 'undefined' && + is.array(schema.paths[k].excludeFromRead) && + !options.readAll + ) { + schema.paths[k].excludeFromRead!.forEach(prop => { + const segments = prop.split('.'); + let v = value; + + while (segments.length > 1 && v !== undefined) { + v = v[segments.shift()!]; + } + + const segment = segments.pop() as string; + + if (v !== undefined && segment in v) { + delete v[segment]; + } + }); + } + } + + data[k] = value; + } + }); + + if (options.showKey) { + data.__key = entityKey; + } else { + delete data.__key; + } + + return data; + }; + + const convertToEntity = (): Entity => { + const key: EntityKey = entityData[Model.gstore.ds.KEY as any]; + return new Model(entityData, undefined, undefined, undefined, key); + }; + + switch (options.format) { + case 'ENTITY': + return convertToEntity() as any; + default: + return convertToJson() as any; + } +}; + +/** + * Convert one or several entities instance (gstore) to Datastore format + * + * @param {any} entities Entity(ies) to format + * @returns {array} the formated entity(ies) + */ +const entitiesToDatastore = ( + entities: T, + options: ToDatastoreOptions | undefined = {}, +): R => { + const isMultiple = is.array(entities); + const entitiesToArray = arrify(entities); + + if (entitiesToArray[0].__className !== 'Entity') { + // Not an entity instance, nothing to do here... + return (entities as unknown) as R; + } + + const result = entitiesToArray.map(e => toDatastore(e, options)); + + return isMultiple ? (result as any) : (result[0] as any); +}; + +export default { + toDatastore, + fromDatastore, + entitiesToDatastore, +}; diff --git a/src/serializers/index.ts b/src/serializers/index.ts new file mode 100644 index 0000000..343e16f --- /dev/null +++ b/src/serializers/index.ts @@ -0,0 +1,3 @@ +import datastore from './datastore'; + +export const datastoreSerializer = datastore; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..92c9971 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,56 @@ +import { entity } from '@google-cloud/datastore/build/src/entity'; +import Entity from './entity'; + +export type EntityKey = entity.Key; + +export type EntityData = { [P in keyof T]: T[P] }; + +export type FuncReturningPromise = (...args: any[]) => Promise; + +export type FunctionType = (...args: any[]) => any; + +export type CustomEntityFunction = (this: Entity, ...args: any[]) => any; + +export type GenericObject = { [key: string]: any }; + +export type IdType = string | number; + +export type Ancestor = (IdType)[]; + +export type EntityFormatType = 'ENTITY'; + +export type JSONFormatType = 'JSON'; + +export type DatastoreSaveMethod = 'upsert' | 'insert' | 'update'; + +export type PopulateRef = { path: string; select: string[] }; + +export type PopulateMetaForEntity = { + entity: Entity | EntityData; + keysToFetch: EntityKey[]; + mapKeyToPropAndSelect: { [key: string]: { ref: PopulateRef } }; +}; + +export type PopulateFunction = ( + entitiesToProcess: null | Entity | Array | EntityData | null>, +) => Promise | EntityData | null | Array | EntityData | null>>; + +export interface PromiseWithPopulate extends Promise { + populate: ( + refs?: U, + properties?: U extends Array ? never : string | string[], + ) => PromiseWithPopulate; +} + +/** + * --------------------------------------------------- + * Google Datastore Types + * --------------------------------------------------- + */ + +// From '@google-cloud/datastore/build/src/query'; +export type DatastoreOperator = '=' | '<' | '>' | '<=' | '>=' | 'HAS_ANCESTOR'; + +export interface OrderOptions { + descending?: boolean; +} diff --git a/src/virtualType.ts b/src/virtualType.ts new file mode 100644 index 0000000..caf6717 --- /dev/null +++ b/src/virtualType.ts @@ -0,0 +1,55 @@ +import is from 'is'; + +import { FunctionType, GenericObject } from './types'; + +class VirtualType { + public readonly name: string; + + public getter: FunctionType | null; + + public setter: FunctionType | null; + + public options: GenericObject; + + constructor(name: string, options?: GenericObject) { + this.name = name; + this.getter = null; + this.setter = null; + this.options = options || {}; + } + + get(fn: FunctionType): VirtualType { + if (!is.fn(fn)) { + throw new Error('You need to pass a function to virtual get'); + } + this.getter = fn; + return this; + } + + set(fn: FunctionType): VirtualType { + if (!is.fn(fn)) { + throw new Error('You need to pass a function to virtual set'); + } + this.setter = fn; + return this; + } + + applyGetters(scope: any): unknown { + if (this.getter === null) { + return null; + } + const v = this.getter.call(scope); + scope[this.name] = v; + return v; + } + + applySetters(value: unknown, scope: any): unknown { + if (this.setter === null) { + return null; + } + const v = this.setter.call(scope, value); + return v; + } +} + +export default VirtualType; diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index f22b7c0..0000000 --- a/test/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "env": { - "node": true, - "mocha": true - } -} \ No newline at end of file diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000..0fedf7a --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + extends: ['../.eslintrc.js'], + rules: { + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + }, + env: { + node: true, + mocha: true, + }, +}; diff --git a/test/dataloader-test.js b/test/dataloader-test.js index 6db6fa3..44e11a4 100644 --- a/test/dataloader-test.js +++ b/test/dataloader-test.js @@ -13,81 +13,78 @@ const { expect, assert } = chai; const ds = new Datastore(); describe('dataloader', () => { - it('should read the ds instance from gstore', () => { - gstore.connect(ds); - const loader = gstore.createDataLoader(); - assert.isDefined(loader); + it('should read the ds instance from gstore', () => { + gstore.connect(ds); + const loader = gstore.createDataLoader(); + assert.isDefined(loader); + }); + + it('should create a dataloader instance', () => { + const loader = createDataLoader(ds); + expect(loader.constructor.name).equal('DataLoader'); + expect(loader._options.maxBatchSize).equal(1000); + }); + + it('should throw an error if no datastore instance passed', () => { + const fn = () => createDataLoader(); + expect(fn).throw('A Datastore instance has to be passed'); + }); + + it('should pass the keys to the datastore "get" method and preserve Order', () => { + const key1 = ds.key(['User', 123]); + const key2 = ds.key(['User', 456]); + const key3 = ds.key({ + namespace: 'ns-test', + path: ['User', 789], }); - it('should create a dataloader instance', () => { - const loader = createDataLoader(ds); - expect(loader.constructor.name).equal('DataLoader'); - expect(loader._options.maxBatchSize).equal(1000); - }); - - it('should throw an error if no datastore instance passed', () => { - const fn = () => createDataLoader(); - expect(fn).throw('A Datastore instance has to be passed'); - }); - - it('should pass the keys to the datastore "get" method and preserve Order', () => { - const key1 = ds.key(['User', 123]); - const key2 = ds.key(['User', 456]); - const key3 = ds.key({ - namespace: 'ns-test', - path: ['User', 789], - }); - - const entity1 = { name: 'John 1' }; - const entity2 = { name: 'John 2' }; - const entity3 = { name: 'John 3' }; + const entity1 = { name: 'John 1' }; + const entity2 = { name: 'John 2' }; + const entity3 = { name: 'John 3' }; - entity1[ds.KEY] = key1; - entity2[ds.KEY] = key2; - entity3[ds.KEY] = key3; + entity1[ds.KEY] = key1; + entity2[ds.KEY] = key2; + entity3[ds.KEY] = key3; - sinon.stub(ds, 'get').resolves([[entity3, entity2, entity1]]); + sinon.stub(ds, 'get').resolves([[entity3, entity2, entity1]]); - const loader = createDataLoader(ds); + const loader = createDataLoader(ds); - return Promise.all([loader.load(key1), loader.load(key2), loader.load(key3)]) - .then(res => { - expect(res[0][ds.KEY].id).equal(123); - expect(res[1][ds.KEY].id).equal(456); - expect(res[2][ds.KEY].id).equal(789); - }); + return Promise.all([loader.load(key1), loader.load(key2), loader.load(key3)]).then(res => { + expect(res[0][ds.KEY].id).equal(123); + expect(res[1][ds.KEY].id).equal(456); + expect(res[2][ds.KEY].id).equal(789); }); + }); - it('should return "null" for entities not found', () => { - const key1 = ds.key(['User', 123]); - const key2 = ds.key(['User', 456]); - const key3 = ds.key(['User', 789]); - const entity = { name: 'John' }; - entity[ds.KEY] = key2; + it('should return "null" for entities not found', () => { + const key1 = ds.key(['User', 123]); + const key2 = ds.key(['User', 456]); + const key3 = ds.key(['User', 789]); + const entity = { name: 'John' }; + entity[ds.KEY] = key2; - ds.get.resolves([[entity]]); + ds.get.resolves([[entity]]); - const loader = createDataLoader(ds); + const loader = createDataLoader(ds); - return Promise.all([loader.load(key1), loader.load(key2), loader.load(key3)]) - .then(res => { - expect(res[0]).equal(null); - expect(res[1][ds.KEY].id).equal(456); - expect(res[2]).equal(null); - }); + return Promise.all([loader.load(key1), loader.load(key2), loader.load(key3)]).then(res => { + expect(res[0]).equal(null); + expect(res[1][ds.KEY].id).equal(456); + expect(res[2]).equal(null); }); + }); - it('should bypass sort if only 1 key', () => { - const entity = { name: 'John' }; - const key = ds.key(['User', 123]); - entity[ds.KEY] = key; - ds.get.resolves([[entity]]); + it('should bypass sort if only 1 key', () => { + const entity = { name: 'John' }; + const key = ds.key(['User', 123]); + entity[ds.KEY] = key; + ds.get.resolves([[entity]]); - const loader = createDataLoader(ds); + const loader = createDataLoader(ds); - return loader.load(key) - .then(res => { - expect(res[ds.KEY].id).equal(123); - }); + return loader.load(key).then(res => { + expect(res[ds.KEY].id).equal(123); }); + }); }); diff --git a/test/entity-test.js b/test/entity-test.js index c80a71a..83a2ead 100755 --- a/test/entity-test.js +++ b/test/entity-test.js @@ -6,1212 +6,1210 @@ const Joi = require('@hapi/joi'); const { Datastore } = require('@google-cloud/datastore'); const ds = new Datastore({ - namespace: 'com.mydomain', - apiEndpoint: 'http://localhost:8080', + namespace: 'com.mydomain', + apiEndpoint: 'http://localhost:8080', }); const Entity = require('../lib/entity'); -const gstoreErrors = require('../lib/errors'); -const datastoreSerializer = require('../lib/serializer').Datastore; +const { ERROR_CODES } = require('../lib/errors'); +const { datastoreSerializer } = require('../lib/serializers'); const { Gstore } = require('../lib'); -const { validation } = require('../lib/helpers'); +const { default: helpers } = require('../lib/helpers'); const Transaction = require('./mocks/transaction'); const gstore = new Gstore(); const gstoreWithCache = new Gstore({ cache: { config: { ttl: { keys: 600 } } } }); const { Schema } = gstore; - const { expect, assert } = chai; +const { validation } = helpers; describe('Entity', () => { - let schema; - let GstoreModel; - let entity; - let transaction; - - beforeEach(() => { - gstore.models = {}; - gstore.modelSchemas = {}; - gstore.options = {}; - gstore.connect(ds); - gstoreWithCache.connect(ds); - - schema = new Schema({ - name: { type: String, default: 'Mick' }, - lastname: { type: String }, - password: { type: String, read: false }, - website: { type: String, validate: 'isURL' }, - }); - - schema.virtual('fullname').get(function getFullName() { - return `${this.name} ${this.lastname}`; - }); - - schema.virtual('fullname').set(function setFullName(name) { - const split = name.split(' '); - [this.name, this.lastname] = split; - }); - - GstoreModel = gstore.model('User', schema); - transaction = new Transaction(); - - sinon.stub(ds, 'save').resolves(); - sinon.spy(transaction, 'save'); + let schema; + let GstoreModel; + let entity; + let transaction; + + beforeEach(() => { + gstore.models = {}; + gstore.modelSchemas = {}; + gstore.options = {}; + gstore.connect(ds); + gstoreWithCache.connect(ds); + + schema = new Schema({ + name: { type: String, default: 'Mick' }, + lastname: { type: String }, + password: { type: String, read: false }, + website: { type: String, validate: 'isURL' }, }); - afterEach(() => { - ds.save.restore(); - transaction.save.restore(); + schema.virtual('fullname').get(function getFullName() { + return `${this.name} ${this.lastname}`; }); - describe('intantiate', () => { - it('should initialized properties', () => { - entity = new GstoreModel({}, 'keyid'); - - assert.isDefined(entity.entityData); - assert.isDefined(entity.entityKey); - assert.isDefined(entity.schema); - assert.isDefined(entity.pre); - assert.isDefined(entity.post); - expect(entity.excludeFromIndexes).deep.equal({}); - }); - - it('should add data passed to entityData', () => { - entity = new GstoreModel({ name: 'John' }); - expect(entity.entityData.name).to.equal('John'); - }); - - it('should have default if no data passed', () => { - entity = new GstoreModel(); - expect(entity.entityData.name).to.equal('Mick'); - }); - - it('should not add any data if nothing is passed', () => { - schema = new Schema({ - name: { type: String, optional: true }, - }); - GstoreModel = gstore.model('BlogPost', schema); - - entity = new GstoreModel(); - - expect(Object.keys(entity.entityData).length).to.equal(0); - }); - - it('should set default values or null from schema', () => { - function fn() { - return 'generatedValue'; - } - - schema = new Schema({ - name: { type: String, default: 'John' }, - lastname: { type: String }, - email: { optional: true }, - generatedValue: { type: String, default: fn }, - availableValues: { values: ['a', 'b', 'c'] }, - availableValuesRequired: { values: ['a', 'b', 'c'], required: true }, - }); - - GstoreModel = gstore.model('BlogPost', schema); - - entity = new GstoreModel({}); - - expect(entity.entityData.name).equal('John'); - expect(entity.entityData.lastname).equal(null); - expect(entity.entityData.email).equal(undefined); - expect(entity.entityData.generatedValue).equal('generatedValue'); - expect(entity.entityData.availableValues).equal('a'); - expect(entity.entityData.availableValuesRequired).equal(null); - }); - - it('should set values from Joi schema', () => { - const generateFullName = context => ( - `${context.name} ${context.lastname}` - ); + schema.virtual('fullname').set(function setFullName(name) { + const split = name.split(' '); + [this.name, this.lastname] = split; + }); - schema = new Schema({ - name: { joi: Joi.string() }, - lastname: { joi: Joi.string().default('Jagger') }, - fullname: { joi: Joi.string().default(generateFullName, 'generated fullname') }, - }, { joi: true }); + GstoreModel = gstore.model('User', schema); + transaction = new Transaction(); - GstoreModel = gstore.model('EntityKind', schema); + sinon.stub(ds, 'save').resolves(); + sinon.spy(transaction, 'save'); + }); - const user = new GstoreModel({ name: 'Mick' }); + afterEach(() => { + ds.save.restore(); + transaction.save.restore(); + }); - expect(user.entityData.lastname).equal('Jagger'); - expect(user.entityData.fullname).equal('Mick Jagger'); - }); + describe('intantiate', () => { + it('should initialized properties', () => { + entity = new GstoreModel({}, 'keyid'); - it('should not set default if Joi validation does not pass', () => { - schema = new Schema({ - name: { joi: Joi.string().default('test').required() }, - lastname: { joi: Joi.string().default('Jagger') }, - age: { joi: Joi.number() }, - }, { joi: true }); + assert.isDefined(entity.entityData); + assert.isDefined(entity.entityKey); + assert.isDefined(entity.schema); + assert.isDefined(entity.pre); + assert.isDefined(entity.post); + expect(entity.__excludeFromIndexes).deep.equal({}); + }); - GstoreModel = gstore.model('EntityKind', schema); + it('should add data passed to entityData', () => { + entity = new GstoreModel({ name: 'John' }); + expect(entity.entityData.name).to.equal('John'); + }); - const user = new GstoreModel({ age: 77 }); + it('should have default if no data passed', () => { + entity = new GstoreModel(); + expect(entity.entityData.name).to.equal('Mick'); + }); - expect(user.age).equal(77); - assert.isUndefined(user.entityData.lastname); - }); + it('should not add any data if nothing is passed', () => { + schema = new Schema({ + name: { type: String, optional: true }, + }); + GstoreModel = gstore.model('BlogPost', schema); - it('should call handler for default values in gstore.defaultValues constants', () => { - sinon.spy(gstore.defaultValues, '__handler__'); - schema = new Schema({ - createdOn: { type: Date, default: gstore.defaultValues.NOW }, - }); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({}); + entity = new GstoreModel(); - expect(gstore.defaultValues.__handler__.calledOnce).equal(true); - return entity; - }); + expect(Object.keys(entity.entityData).length).to.equal(0); + }); - it('should not add default to optional properties', () => { - schema = new Schema({ - name: { type: String }, - email: { optional: true }, - }); - GstoreModel = gstore.model('BlogPost', schema); + it('should set default values or null from schema', () => { + function fn() { + return 'generatedValue'; + } + + schema = new Schema({ + name: { type: String, default: 'John' }, + lastname: { type: String }, + email: { optional: true }, + generatedValue: { type: String, default: fn }, + availableValues: { values: ['a', 'b', 'c'] }, + availableValuesRequired: { values: ['a', 'b', 'c'], required: true }, + }); + + GstoreModel = gstore.model('BlogPost', schema); + + entity = new GstoreModel({}); + + expect(entity.entityData.name).equal('John'); + expect(entity.entityData.lastname).equal(null); + expect(entity.entityData.email).equal(undefined); + expect(entity.entityData.generatedValue).equal('generatedValue'); + expect(entity.entityData.availableValues).equal('a'); + expect(entity.entityData.availableValuesRequired).equal(null); + }); - entity = new GstoreModel({}); + it('should set values from Joi schema', () => { + const generateFullName = context => `${context.name} ${context.lastname}`; - expect(entity.entityData.email).equal(undefined); - }); + schema = new Schema( + { + name: { joi: Joi.string() }, + lastname: { joi: Joi.string().default('Jagger') }, + fullname: { joi: Joi.string().default(generateFullName, 'generated fullname') }, + }, + { joi: true }, + ); - it('should create its array of excludeFromIndexes', () => { - schema = new Schema({ - name: { excludeFromIndexes: true }, - age: { excludeFromIndexes: true, type: Number }, - embedded: { type: Object, excludeFromIndexes: ['prop1', 'prop2'] }, - embedded2: { type: Object, excludeFromIndexes: true }, - arrayValue: { excludeFromIndexes: 'property', type: Array }, - // Array in @google-cloud have to be set on the data value - arrayValue2: { excludeFromIndexes: true, type: Array }, - arrayValue3: { excludeFromIndexes: true, joi: Joi.array() }, - }); - GstoreModel = gstore.model('BlogPost', schema); - - entity = new GstoreModel({ name: 'John' }); - - expect(entity.excludeFromIndexes).deep.equal({ - name: ['name'], - age: ['age'], - embedded: ['embedded.prop1', 'embedded.prop2'], - embedded2: ['embedded2', 'embedded2.*'], - arrayValue: ['arrayValue[].property'], - arrayValue2: ['arrayValue2[]', 'arrayValue2[].*'], - arrayValue3: ['arrayValue3[]', 'arrayValue3[].*'], - }); - }); + GstoreModel = gstore.model('EntityKind', schema); - describe('should create Datastore Key', () => { - beforeEach(() => { - sinon.spy(ds, 'key'); + const user = new GstoreModel({ name: 'Mick' }); - GstoreModel = gstore.model('BlogPost', schema); - }); + expect(user.entityData.lastname).equal('Jagger'); + expect(user.entityData.fullname).equal('Mick Jagger'); + }); - afterEach(() => { - ds.key.restore(); - }); + it('should not set default if Joi validation does not pass', () => { + schema = new Schema( + { + name: { + joi: Joi.string() + .default('test') + .required(), + }, + lastname: { joi: Joi.string().default('Jagger') }, + age: { joi: Joi.number() }, + }, + { joi: true }, + ); + + GstoreModel = gstore.model('EntityKind', schema); + + const user = new GstoreModel({ age: 77 }); + + expect(user.age).equal(77); + assert.isUndefined(user.entityData.lastname); + }); - it('---> with a full Key (String keyname passed)', () => { - entity = new GstoreModel({}, 'keyid'); + it('should call handler for default values in gstore.defaultValues constants', () => { + sinon.spy(gstore.defaultValues, '__handler__'); + schema = new Schema({ + createdOn: { type: Date, default: gstore.defaultValues.NOW }, + }); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({}); - expect(entity.entityKey.kind).equal('BlogPost'); - expect(entity.entityKey.name).equal('keyid'); - }); + expect(gstore.defaultValues.__handler__.calledOnce).equal(true); + return entity; + }); - it('---> with a full Key (String with including numbers)', () => { - entity = new GstoreModel({}, '123:456'); + it('should not add default to optional properties', () => { + schema = new Schema({ + name: { type: String }, + email: { optional: true }, + }); + GstoreModel = gstore.model('BlogPost', schema); - expect(entity.entityKey.name).equal('123:456'); - }); + entity = new GstoreModel({}); - it('---> with a full Key (Integer keyname passed)', () => { - entity = new GstoreModel({}, 123); + expect(entity.entityData.email).equal(undefined); + }); - expect(entity.entityKey.id).equal(123); - }); + it('should create its array of excludeFromIndexes', () => { + schema = new Schema({ + name: { excludeFromIndexes: true }, + age: { excludeFromIndexes: true, type: Number }, + embedded: { type: Object, excludeFromIndexes: ['prop1', 'prop2'] }, + embedded2: { type: Object, excludeFromIndexes: true }, + arrayValue: { excludeFromIndexes: 'property', type: Array }, + // Array in @google-cloud have to be set on the data value + arrayValue2: { excludeFromIndexes: true, type: Array }, + arrayValue3: { excludeFromIndexes: true, joi: Joi.array() }, + }); + GstoreModel = gstore.model('BlogPost', schema); + + entity = new GstoreModel({ name: 'John' }); + + expect(entity.__excludeFromIndexes).deep.equal({ + name: ['name'], + age: ['age'], + embedded: ['embedded.prop1', 'embedded.prop2'], + embedded2: ['embedded2', 'embedded2.*'], + arrayValue: ['arrayValue[].property'], + arrayValue2: ['arrayValue2[]', 'arrayValue2[].*'], + arrayValue3: ['arrayValue3[]', 'arrayValue3[].*'], + }); + }); - it('---> with a full Key ("string" Integer keyname passed)', () => { - entity = new GstoreModel({}, '123'); - expect(entity.entityKey.name).equal('123'); - }); + describe('should create Datastore Key', () => { + beforeEach(() => { + sinon.spy(ds, 'key'); - it('---> throw error is id passed is not string or number', () => { - const fn = () => { - entity = new GstoreModel({}, {}); - return entity; - }; + GstoreModel = gstore.model('BlogPost', schema); + }); - expect(fn).throw(Error); - }); + afterEach(() => { + ds.key.restore(); + }); - it('---> with a partial Key (auto-generated id)', () => { - entity = new GstoreModel({}); + it('---> with a full Key (String keyname passed)', () => { + entity = new GstoreModel({}, 'keyid'); - expect(entity.entityKey.kind).to.deep.equal('BlogPost'); - }); + expect(entity.entityKey.kind).equal('BlogPost'); + expect(entity.entityKey.name).equal('keyid'); + }); - it('---> with an ancestor path (auto-generated id)', () => { - entity = new GstoreModel({}, null, ['Parent', 1234]); + it('---> with a full Key (String with including numbers)', () => { + entity = new GstoreModel({}, '123:456'); - expect(entity.entityKey.parent.kind).equal('Parent'); - expect(entity.entityKey.parent.id).equal(1234); - expect(entity.entityKey.kind).equal('BlogPost'); - }); + expect(entity.entityKey.name).equal('123:456'); + }); - it('---> with an ancestor path (manual id)', () => { - entity = new GstoreModel({}, 'entityKind', ['Parent', 1234]); + it('---> with a full Key (Integer keyname passed)', () => { + entity = new GstoreModel({}, 123); - expect(entity.entityKey.parent.kind).equal('Parent'); - expect(entity.entityKey.parent.id).equal(1234); - expect(entity.entityKey.kind).equal('BlogPost'); - expect(entity.entityKey.name).equal('entityKind'); - }); + expect(entity.entityKey.id).equal(123); + }); - it('---> with a namespace', () => { - entity = new GstoreModel({}, null, null, 'com.otherdomain'); + it('---> with a full Key ("string" Integer keyname passed)', () => { + entity = new GstoreModel({}, '123'); + expect(entity.entityKey.name).equal('123'); + }); - expect(entity.entityKey.namespace).equal('com.otherdomain'); - }); + it('---> throw error is id passed is not string or number', () => { + const fn = () => { + entity = new GstoreModel({}, {}); + return entity; + }; - it('---> with a gcloud Key', () => { - const key = ds.key('BlogPost', 1234); + expect(fn).throw(Error); + }); - entity = new GstoreModel({}, null, null, null, key); + it('---> with a partial Key (auto-generated id)', () => { + entity = new GstoreModel({}); - expect(entity.entityKey).equal(key); - }); + expect(entity.entityKey.kind).to.deep.equal('BlogPost'); + }); - it('---> throw error if key is not instance of Key', () => { - function fn() { - entity = new GstoreModel({}, null, null, null, {}); - return entity; - } + it('---> with an ancestor path (auto-generated id)', () => { + entity = new GstoreModel({}, null, ['Parent', 1234]); - expect(fn).to.throw(); - }); - }); + expect(entity.entityKey.parent.kind).equal('Parent'); + expect(entity.entityKey.parent.id).equal(1234); + expect(entity.entityKey.kind).equal('BlogPost'); + }); - describe('should register schema hooks', () => { - let spyOn; + it('---> with an ancestor path (manual id)', () => { + entity = new GstoreModel({}, 'entityKind', ['Parent', 1234]); - beforeEach(() => { - spyOn = { - fnHookPre: () => Promise.resolve(), - fnHookPost: () => Promise.resolve({ __override: 1234 }), - }; + expect(entity.entityKey.parent.kind).equal('Parent'); + expect(entity.entityKey.parent.id).equal(1234); + expect(entity.entityKey.kind).equal('BlogPost'); + expect(entity.entityKey.name).equal('entityKind'); + }); - sinon.spy(spyOn, 'fnHookPre'); - sinon.spy(spyOn, 'fnHookPost'); - }); + it('---> with a namespace', () => { + entity = new GstoreModel({}, null, null, 'com.otherdomain'); - afterEach(() => { - spyOn.fnHookPost.restore(); - spyOn.fnHookPre.restore(); - }); + expect(entity.entityKey.namespace).equal('com.otherdomain'); + }); - it('should call pre hooks before saving and override arguments', () => { - schema.pre('save', spyOn.fnHookPre); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({ name: 'John' }); + it('---> with a gcloud Key', () => { + const key = ds.key('BlogPost', 1234); - return entity.save().then(() => { - expect(spyOn.fnHookPre.callCount).to.equal(1); - }); - }); + entity = new GstoreModel({}, null, null, null, key); - it('should call pre and post hooks on custom method', () => { - schema.method('newmethod', () => Promise.resolve()); - schema.pre('newmethod', spyOn.fnHookPre); - schema.post('newmethod', spyOn.fnHookPost); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({ name: 'John' }); - - return entity.newmethod().then(() => { - expect(spyOn.fnHookPre.callCount).to.equal(1); - expect(spyOn.fnHookPost.callCount).to.equal(1); - }); - }); + expect(entity.entityKey).equal(key); + }); - it('should call post hooks after saving and override resolve', () => { - schema.post('save', spyOn.fnHookPost); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({}); + it('---> throw error if key is not instance of Key', () => { + function fn() { + entity = new GstoreModel({}, null, null, null, {}); + return entity; + } - return entity.save().then(result => { - expect(spyOn.fnHookPost.called).equal(true); - expect(result).equal(1234); - }); - }); + expect(fn).to.throw(); + }); + }); - it('should not do anything if no hooks on schema', () => { - schema.callQueue = { model: {}, entity: {} }; - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({ name: 'John' }); + describe('should register schema hooks', () => { + let spyOn; - assert.isUndefined(entity.__pres); - assert.isUndefined(entity.__posts); - }); + beforeEach(() => { + spyOn = { + fnHookPre: () => Promise.resolve(), + fnHookPost: () => Promise.resolve({ __override: 1234 }), + }; - it('should not register unknown methods', () => { - schema.callQueue = { model: {}, entity: {} }; - schema.pre('unknown', () => { }); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({}); + sinon.spy(spyOn, 'fnHookPre'); + sinon.spy(spyOn, 'fnHookPost'); + }); - assert.isUndefined(entity.__pres); - assert.isUndefined(entity.__posts); - }); - }); - }); + afterEach(() => { + spyOn.fnHookPost.restore(); + spyOn.fnHookPre.restore(); + }); - describe('get / set', () => { - let user; + it('should call pre hooks before saving and override arguments', () => { + schema.pre('save', spyOn.fnHookPre); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({ name: 'John' }); - beforeEach(() => { - user = new GstoreModel({ name: 'John', lastname: 'Snow' }); + return entity.save().then(() => { + expect(spyOn.fnHookPre.callCount).to.equal(1); }); + }); - it('should get an entityData property', () => { - const name = user.get('name'); + it('should call pre and post hooks on custom method', () => { + schema.method('newmethod', () => Promise.resolve()); + schema.pre('newmethod', spyOn.fnHookPre); + schema.post('newmethod', spyOn.fnHookPost); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({ name: 'John' }); - expect(name).equal('John'); + return entity.newmethod().then(() => { + expect(spyOn.fnHookPre.callCount).to.equal(1); + expect(spyOn.fnHookPost.callCount).to.equal(1); }); + }); - it('should return virtual', () => { - const fullname = user.get('fullname'); + it('should call post hooks after saving and override resolve', () => { + schema.post('save', spyOn.fnHookPost); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({}); - expect(fullname).equal('John Snow'); + return entity.save().then(result => { + expect(spyOn.fnHookPost.called).equal(true); + expect(result).equal(1234); }); + }); - it('should set an entityData property', () => { - user.set('name', 'Gregory'); - - const name = user.get('name'); - - expect(name).equal('Gregory'); - }); + it('should not do anything if no hooks on schema', () => { + schema.callQueue = { model: {}, entity: {} }; + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({ name: 'John' }); - it('should set virtual', () => { - user.set('fullname', 'Peter Jackson'); + assert.isUndefined(entity.__pres); + assert.isUndefined(entity.__posts); + }); - expect(user.entityData.name).equal('Peter'); - }); + it('should not register unknown methods', () => { + schema.callQueue = { model: {}, entity: {} }; + schema.pre('unknown', () => {}); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({}); - it('should get data on entity properties from the entity data', () => { - GstoreModel = gstore.model('BlogPost', schema); + assert.isUndefined(entity.__pres); + assert.isUndefined(entity.__posts); + }); + }); + }); - entity = new GstoreModel({ - name: 'Jane', - lastname: 'Does', - password: 'JanesPassword', - }); + describe('get / set', () => { + let user; - expect(entity.name).to.equal('Jane'); - expect(entity.lastname).to.equal('Does'); - expect(entity.password).to.equal('JanesPassword'); - }); + beforeEach(() => { + user = new GstoreModel({ name: 'John', lastname: 'Snow' }); + }); - it('should reflect changes to entity properties in the entity data', () => { - GstoreModel = gstore.model('BlogPost', schema); + it('should get an entityData property', () => { + const name = user.get('name'); - entity = new GstoreModel({ - name: 'Jane', - lastname: 'Does', - password: 'JanesPassword', - }); + expect(name).equal('John'); + }); - entity.name = 'John'; - entity.lastname = 'Doe'; - entity.password = 'JoesPassword'; + it('should return virtual', () => { + const fullname = user.get('fullname'); - expect(entity.entityData.name).to.equal('John'); - expect(entity.entityData.lastname).to.equal('Doe'); - expect(entity.entityData.password).to.equal('JoesPassword'); - }); + expect(fullname).equal('John Snow'); }); - describe('plain()', () => { - beforeEach(() => { - sinon.spy(datastoreSerializer, 'fromDatastore'); - }); + it('should set an entityData property', () => { + user.set('name', 'Gregory'); - afterEach(() => { - datastoreSerializer.fromDatastore.restore(); - }); + const name = user.get('name'); - it('should throw an error is options is not of type Object', () => { - const fn = () => { - entity = new GstoreModel({ name: 'John' }); - entity.plain(true); - }; + expect(name).equal('Gregory'); + }); - expect(fn).throw(Error); - }); + it('should set virtual', () => { + user.set('fullname', 'Peter Jackson'); - it('should call datastoreSerializer "fromDatastore"', () => { - entity = new GstoreModel({ name: 'John', password: 'test' }); - const { entityData } = entity; + expect(user.entityData.name).equal('Peter'); + }); - const output = entity.plain(); + it('should get data on entity properties from the entity data', () => { + GstoreModel = gstore.model('BlogPost', schema); - expect(datastoreSerializer.fromDatastore.getCall(0).args[0]).deep.equal(entityData); - expect(datastoreSerializer.fromDatastore.getCall(0).args[1]).deep.equal({ readAll: false, showKey: false }); - assert.isUndefined(output.password); - }); + entity = new GstoreModel({ + name: 'Jane', + lastname: 'Does', + password: 'JanesPassword', + }); - it('should call datastoreSerializer "fromDatastore" passing readAll parameter', () => { - entity = new GstoreModel({ name: 'John', password: 'test' }); + expect(entity.name).to.equal('Jane'); + expect(entity.lastname).to.equal('Does'); + expect(entity.password).to.equal('JanesPassword'); + }); - const output = entity.plain({ readAll: true }); + it('should reflect changes to entity properties in the entity data', () => { + GstoreModel = gstore.model('BlogPost', schema); - expect(datastoreSerializer.fromDatastore.getCall(0).args[1]).deep.equal({ readAll: true, showKey: false }); - assert.isDefined(output.password); - }); + entity = new GstoreModel({ + name: 'Jane', + lastname: 'Does', + password: 'JanesPassword', + }); - it('should pass showKey parameter', () => { - entity = new GstoreModel({}); + entity.name = 'John'; + entity.lastname = 'Doe'; + entity.password = 'JoesPassword'; - entity.plain({ showKey: true }); + expect(entity.entityData.name).to.equal('John'); + expect(entity.entityData.lastname).to.equal('Doe'); + expect(entity.entityData.password).to.equal('JoesPassword'); + }); + }); - expect(datastoreSerializer.fromDatastore.getCall(0).args[1]).deep.equal({ readAll: false, showKey: true }); - }); + describe('plain()', () => { + beforeEach(() => { + sinon.spy(datastoreSerializer, 'fromDatastore'); + }); - it('should add virtuals', () => { - entity = new GstoreModel({ name: 'John' }); - sinon.spy(entity, 'getEntityDataWithVirtuals'); + afterEach(() => { + datastoreSerializer.fromDatastore.restore(); + }); - entity.plain({ virtuals: true }); + it('should throw an error is options is not of type Object', () => { + const fn = () => { + entity = new GstoreModel({ name: 'John' }); + entity.plain(true); + }; - expect(entity.getEntityDataWithVirtuals.called).equal(true); - }); + expect(fn).throw(Error); + }); - it('should clear embedded object excluded properties', () => { - schema = new Schema({ - embedded: { excludeFromRead: ['prop1', 'prop2'] }, - }); + it('should call datastoreSerializer "fromDatastore"', () => { + entity = new GstoreModel({ name: 'John', password: 'test' }); + const { entityData } = entity; - GstoreModel = gstore.model('HasEmbedded', schema); + const output = entity.plain(); - entity = new GstoreModel({ embedded: { prop1: '1', prop2: '2', prop3: '3' } }); - const plain = entity.plain({}); + expect(datastoreSerializer.fromDatastore.getCall(0).args[0]).deep.equal(entityData); + expect(datastoreSerializer.fromDatastore.getCall(0).args[2]).deep.equal({ readAll: false, showKey: false }); + assert.isUndefined(output.password); + }); - assert.isUndefined(plain.embedded.prop1); - assert.isUndefined(plain.embedded.prop2); - expect(plain.embedded.prop3).equal('3'); - }); + it('should call datastoreSerializer "fromDatastore" passing readAll parameter', () => { + entity = new GstoreModel({ name: 'John', password: 'test' }); - it('should clear nested embedded object excluded properties', () => { - schema = new Schema({ - embedded: { excludeFromRead: ['prop1', 'prop2.p1', 'prop3.p1.p11'] }, - }); + const output = entity.plain({ readAll: true }); - GstoreModel = gstore.model('HasEmbedded', schema); + expect(datastoreSerializer.fromDatastore.getCall(0).args[2]).deep.equal({ readAll: true, showKey: false }); + assert.isDefined(output.password); + }); - entity = new GstoreModel({ - embedded: { - prop1: '1', - prop2: { p1: 'p1', p2: 'p2' }, - prop3: { p1: { p11: 'p11', p12: 'p12' }, p2: 'p2' }, - prop4: '4', - }, - }); + it('should pass showKey parameter', () => { + entity = new GstoreModel({}); - const plain = entity.plain({}); - - assert.isUndefined(plain.embedded.prop1); - expect(typeof plain.embedded.prop2).equal('object'); - assert.isUndefined(plain.embedded.prop2.p1); - expect(plain.embedded.prop2.p2).equal('p2'); - expect(typeof plain.embedded.prop3).equal('object'); - expect(typeof plain.embedded.prop3.p1).equal('object'); - assert.isUndefined(plain.embedded.prop3.p1.p11); - expect(plain.embedded.prop3.p1.p12).equal('p12'); - expect(plain.embedded.prop3.p2).equal('p2'); - expect(plain.embedded.prop4).equal('4'); - }); + entity.plain({ showKey: true }); - it('should ignore incorrectly specified nested embedded object property paths', () => { - schema = new Schema({ - embedded: { excludeFromRead: ['prop3.wrong.p1', 'prop4', 'prop4.p1.p2', 'prop5.p1'] }, - }); + expect(datastoreSerializer.fromDatastore.getCall(0).args[2]).deep.equal({ readAll: false, showKey: true }); + }); - GstoreModel = gstore.model('HasEmbedded', schema); + it('should add virtuals', () => { + entity = new GstoreModel({ name: 'John' }); + sinon.spy(entity, '__getEntityDataWithVirtuals'); - entity = new GstoreModel({ - embedded: { - prop1: '1', - prop2: { p1: { p2: 'p2' } }, - prop3: { p1: { p2: { p3: 'p3' } } }, - }, - }); + entity.plain({ virtuals: true }); - const plain = entity.plain(); + expect(entity.__getEntityDataWithVirtuals.called).equal(true); + entity.__getEntityDataWithVirtuals.restore(); + }); - expect(plain.embedded.prop1).equal('1'); - expect(plain.embedded.prop2.p1.p2).equal('p2'); - expect(plain.embedded.prop3.p1.p2.p3).equal('p3'); - }); + it('should clear embedded object excluded properties', () => { + schema = new Schema({ + embedded: { excludeFromRead: ['prop1', 'prop2'] }, + }); - it('should not clear nested embedded object excluded properties when specifying readAll: true', () => { - schema = new Schema({ - embedded: { excludeFromRead: ['prop1', 'prop2.p1', 'prop3.p1.p11'] }, - }); + GstoreModel = gstore.model('HasEmbedded', schema); - GstoreModel = gstore.model('HasEmbedded', schema); + entity = new GstoreModel({ embedded: { prop1: '1', prop2: '2', prop3: '3' } }); + const plain = entity.plain({}); - entity = new GstoreModel({ - embedded: { - prop1: '1', - prop2: { p1: 'p1', p2: 'p2' }, - prop3: { p1: { p11: 'p11', p12: 'p12' }, p2: 'p2' }, - prop4: '4', - }, - }); + assert.isUndefined(plain.embedded.prop1); + assert.isUndefined(plain.embedded.prop2); + expect(plain.embedded.prop3).equal('3'); + }); - const plain = entity.plain({ readAll: true }); - - expect(typeof plain.embedded.prop1).equal('string'); - expect(typeof plain.embedded.prop2).equal('object'); - expect(typeof plain.embedded.prop3).equal('object'); - expect(typeof plain.embedded.prop4).equal('string'); - expect(plain.embedded.prop1).equal('1'); - expect(plain.embedded.prop2.p1).equal('p1'); - expect(plain.embedded.prop2.p2).equal('p2'); - expect(plain.embedded.prop3.p1.p11).equal('p11'); - expect(plain.embedded.prop3.p1.p12).equal('p12'); - expect(plain.embedded.prop3.p2).equal('p2'); - expect(plain.embedded.prop4).equal('4'); - }); + it('should clear nested embedded object excluded properties', () => { + schema = new Schema({ + embedded: { excludeFromRead: ['prop1', 'prop2.p1', 'prop3.p1.p11'] }, + }); + + GstoreModel = gstore.model('HasEmbedded', schema); + + entity = new GstoreModel({ + embedded: { + prop1: '1', + prop2: { p1: 'p1', p2: 'p2' }, + prop3: { p1: { p11: 'p11', p12: 'p12' }, p2: 'p2' }, + prop4: '4', + }, + }); + + const plain = entity.plain({}); + + assert.isUndefined(plain.embedded.prop1); + expect(typeof plain.embedded.prop2).equal('object'); + assert.isUndefined(plain.embedded.prop2.p1); + expect(plain.embedded.prop2.p2).equal('p2'); + expect(typeof plain.embedded.prop3).equal('object'); + expect(typeof plain.embedded.prop3.p1).equal('object'); + assert.isUndefined(plain.embedded.prop3.p1.p11); + expect(plain.embedded.prop3.p1.p12).equal('p12'); + expect(plain.embedded.prop3.p2).equal('p2'); + expect(plain.embedded.prop4).equal('4'); }); - describe('datastoreEntity()', () => { - it('should get the data from the Datastore and merge it into the entity', () => { - const mockData = { name: 'John' }; - sinon.stub(ds, 'get').resolves([mockData]); + it('should ignore incorrectly specified nested embedded object property paths', () => { + schema = new Schema({ + embedded: { excludeFromRead: ['prop3.wrong.p1', 'prop4', 'prop4.p1.p2', 'prop5.p1'] }, + }); - entity = new GstoreModel({}); + GstoreModel = gstore.model('HasEmbedded', schema); - return entity.datastoreEntity().then(_entity => { - expect(ds.get.called).equal(true); - expect(ds.get.getCall(0).args[0]).equal(entity.entityKey); - expect(_entity.className).equal('Entity'); - expect(_entity.entityData).equal(mockData); + entity = new GstoreModel({ + embedded: { + prop1: '1', + prop2: { p1: { p2: 'p2' } }, + prop3: { p1: { p2: { p3: 'p3' } } }, + }, + }); - ds.get.restore(); - }); - }); + const plain = entity.plain(); - it('should return 404 not found if no entity returned', () => { - sinon.stub(ds, 'get').resolves([]); + expect(plain.embedded.prop1).equal('1'); + expect(plain.embedded.prop2.p1.p2).equal('p2'); + expect(plain.embedded.prop3.p1.p2.p3).equal('p3'); + }); - entity = new GstoreModel({}); + it('should not clear nested embedded object excluded properties when specifying readAll: true', () => { + schema = new Schema({ + embedded: { excludeFromRead: ['prop1', 'prop2.p1', 'prop3.p1.p11'] }, + }); + + GstoreModel = gstore.model('HasEmbedded', schema); + + entity = new GstoreModel({ + embedded: { + prop1: '1', + prop2: { p1: 'p1', p2: 'p2' }, + prop3: { p1: { p11: 'p11', p12: 'p12' }, p2: 'p2' }, + prop4: '4', + }, + }); + + const plain = entity.plain({ readAll: true }); + + expect(typeof plain.embedded.prop1).equal('string'); + expect(typeof plain.embedded.prop2).equal('object'); + expect(typeof plain.embedded.prop3).equal('object'); + expect(typeof plain.embedded.prop4).equal('string'); + expect(plain.embedded.prop1).equal('1'); + expect(plain.embedded.prop2.p1).equal('p1'); + expect(plain.embedded.prop2.p2).equal('p2'); + expect(plain.embedded.prop3.p1.p11).equal('p11'); + expect(plain.embedded.prop3.p1.p12).equal('p12'); + expect(plain.embedded.prop3.p2).equal('p2'); + expect(plain.embedded.prop4).equal('4'); + }); + }); - return entity.datastoreEntity().catch(err => { - expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); - expect(err.message).equal('Entity not found'); - ds.get.restore(); - }); - }); + describe('datastoreEntity()', () => { + it('should get the data from the Datastore and merge it into the entity', () => { + const mockData = { name: 'John' }; + sinon.stub(ds, 'get').resolves([mockData]); - it('should return 404 not found if no entity returned (2)', () => { - sinon.stub(ds, 'get').resolves(); + entity = new GstoreModel({}); - entity = new GstoreModel({}); + return entity.datastoreEntity().then(_entity => { + expect(ds.get.called).equal(true); + expect(ds.get.getCall(0).args[0]).equal(entity.entityKey); + expect(_entity.__className).equal('Entity'); + expect(_entity.entityData).equal(mockData); - return entity.datastoreEntity().catch(err => { - expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); - ds.get.restore(); - }); - }); + ds.get.restore(); + }); + }); - it('should return null if no entity returned', () => { - gstore.config.errorOnEntityNotFound = false; + it('should return 404 not found if no entity returned', () => { + sinon.stub(ds, 'get').resolves([]); - sinon.stub(ds, 'get').resolves([]); + entity = new GstoreModel({}); - entity = new GstoreModel({}); + return entity.datastoreEntity().catch(err => { + expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); + expect(err.message).equal('Entity not found'); + ds.get.restore(); + }); + }); - return entity.datastoreEntity().then(_entity => { - expect(_entity).equal(null); - ds.get.restore(); - }); - }); + it('should return 404 not found if no entity returned (2)', () => { + sinon.stub(ds, 'get').resolves(); - it('should bubble up error fetching the entity', () => { - const error = { code: 500, message: 'Something went bad' }; - sinon.stub(ds, 'get').rejects(error); + entity = new GstoreModel({}); - entity = new GstoreModel({}); + return entity.datastoreEntity().catch(err => { + expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); + ds.get.restore(); + }); + }); - return entity.datastoreEntity().catch(err => { - expect(err).equal(error); + it('should return null if no entity returned', () => { + gstore.config.errorOnEntityNotFound = false; - ds.get.restore(); - }); - }); + sinon.stub(ds, 'get').resolves([]); - context('when cache is active', () => { - let key; - let mockData; + entity = new GstoreModel({}); - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; + return entity.datastoreEntity().then(_entity => { + expect(_entity).equal(null); + ds.get.restore(); + }); + }); - key = GstoreModel.key(123); - mockData = { name: 'John' }; - mockData[gstore.ds.KEY] = key; - }); + it('should bubble up error fetching the entity', () => { + const error = { code: 500, message: 'Something went bad' }; + sinon.stub(ds, 'get').rejects(error); - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); + entity = new GstoreModel({}); - it('should get value from cache', () => { - const value = mockData; - entity = new GstoreModel(mockData); - entity.entityKey = key; - - sinon.spy(entity.gstore.cache.keys, 'read'); - sinon.stub(ds, 'get').resolves([mockData]); - - return gstore.cache.keys.set(key, value) - .then(() => ( - entity.datastoreEntity({ ttl: 123456 }) - .then(response => { - assert.ok(!ds.get.called); - expect(response.entityData).include(value); - assert.ok(entity.gstore.cache.keys.read.called); - const { args } = entity.gstore.cache.keys.read.getCall(0); - expect(args[0]).equal(key); - expect(args[1].ttl).equal(123456); - - entity.gstore.cache.keys.read.restore(); - ds.get.restore(); - }) - )); - }); + return entity.datastoreEntity().catch(err => { + expect(err).equal(error); - it('should **not** get value from cache', () => { - const value = mockData; - entity = new GstoreModel(mockData); - entity.entityKey = key; - - sinon.spy(entity.gstore.cache.keys, 'read'); - sinon.stub(ds, 'get').resolves([mockData]); - - return gstore.cache.keys.set(key, value) - .then(() => ( - entity.datastoreEntity({ cache: false }) - .then(() => { - assert.ok(ds.get.called); - assert.ok(!entity.gstore.cache.keys.read.called); - - entity.gstore.cache.keys.read.restore(); - ds.get.restore(); - }) - )); - }); - }); + ds.get.restore(); + }); + }); + + context('when cache is active', () => { + let key; + let mockData; + + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + + key = GstoreModel.key(123); + mockData = { name: 'John' }; + mockData[gstore.ds.KEY] = key; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should get value from cache', () => { + const value = mockData; + entity = new GstoreModel(mockData); + entity.entityKey = key; + + sinon.spy(entity.gstore.cache.keys, 'read'); + sinon.stub(ds, 'get').resolves([mockData]); + + return gstore.cache.keys.set(key, value).then(() => + entity.datastoreEntity({ ttl: 123456 }).then(response => { + assert.ok(!ds.get.called); + expect(response.entityData).include(value); + assert.ok(entity.gstore.cache.keys.read.called); + const { args } = entity.gstore.cache.keys.read.getCall(0); + expect(args[0]).equal(key); + expect(args[1].ttl).equal(123456); + + entity.gstore.cache.keys.read.restore(); + ds.get.restore(); + }), + ); + }); + + it('should **not** get value from cache', () => { + const value = mockData; + entity = new GstoreModel(mockData); + entity.entityKey = key; + + sinon.spy(entity.gstore.cache.keys, 'read'); + sinon.stub(ds, 'get').resolves([mockData]); + + return gstore.cache.keys.set(key, value).then(() => + entity.datastoreEntity({ cache: false }).then(() => { + assert.ok(ds.get.called); + assert.ok(!entity.gstore.cache.keys.read.called); + + entity.gstore.cache.keys.read.restore(); + ds.get.restore(); + }), + ); + }); }); + }); - describe('model()', () => { - it('should be able to return model instances', () => { - const imageSchema = new Schema({}); - const ImageModel = gstore.model('Image', imageSchema); + describe('model()', () => { + it('should be able to return model instances', () => { + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); - const blog = new GstoreModel({}); + const blog = new GstoreModel({}); - expect(blog.model('Image')).equal(ImageModel); - }); + expect(blog.model('Image')).equal(ImageModel); + }); - it('should be able to execute methods from other model instances', () => { - const imageSchema = new Schema({}); - const ImageModel = gstore.model('Image', imageSchema); - const mockEntities = [{ key: ds.key(['BlogPost', 1234]) }]; + it('should be able to execute methods from other model instances', () => { + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); + const mockEntities = [{ key: ds.key(['BlogPost', 1234]) }]; - sinon.stub(ImageModel, 'get').callsFake(() => Promise.resolve(mockEntities[0])); + sinon.stub(ImageModel, 'get').callsFake(() => Promise.resolve(mockEntities[0])); - const blog = new GstoreModel({}); + const blog = new GstoreModel({}); - return blog.model('Image') - .get() - .then(_entity => { - expect(_entity).equal(mockEntities[0]); - }); + return blog + .model('Image') + .get() + .then(_entity => { + expect(_entity).equal(mockEntities[0]); }); }); + }); - describe('getEntityDataWithVirtuals()', () => { - let model; - let User; - - beforeEach(() => { - schema = new Schema({ firstname: {}, lastname: {} }); + describe('getEntityDataWithVirtuals()', () => { + let User; - schema.virtual('fullname').get(function getFullName() { - return `${this.firstname} ${this.lastname}`; - }); + beforeEach(() => { + schema = new Schema({ firstname: {}, lastname: {} }); - schema.virtual('fullname').set(function setFullName(name) { - const split = name.split(' '); - [this.firstname, this.lastname] = split; - }); + schema.virtual('fullname').get(function getFullName() { + return `${this.firstname} ${this.lastname}`; + }); - User = gstore.model('Client', schema); + schema.virtual('fullname').set(function setFullName(name) { + const split = name.split(' '); + [this.firstname, this.lastname] = split; + }); - model = new User({ firstname: 'John', lastname: 'Snow' }); - }); + User = gstore.model('Client', schema); - it('should add add virtuals on instance', () => { - assert.isDefined(model.fullname); - }); - - it('setting on instance should modify entityData', () => { - expect(model.fullname).equal('John Snow'); - }); + entity = new User({ firstname: 'John', lastname: 'Snow' }); + }); - it('should add virtuals properties on entity instance', () => { - expect(model.fullname).equal('John Snow'); - model.firstname = 'Mick'; - expect(model.fullname).equal('Mick Snow'); - model.fullname = 'Andre Agassi'; - expect(model.firstname).equal('Andre'); - expect(model.lastname).equal('Agassi'); - expect(model.entityData).deep.equal({ firstname: 'Andre', lastname: 'Agassi' }); - }); + it('should add add virtuals on instance', () => { + assert.isDefined(entity.fullname); + }); - it('should Not override', () => { - model = new User({ firstname: 'John', lastname: 'Snow', fullname: 'Jooohn' }); - const entityData = model.getEntityDataWithVirtuals(); + it('setting on instance should modify entityData', () => { + expect(entity.fullname).equal('John Snow'); + }); - expect(entityData.fullname).equal('Jooohn'); - }); + it('should add virtuals properties on entity instance', () => { + expect(entity.fullname).equal('John Snow'); + entity.firstname = 'Mick'; + expect(entity.fullname).equal('Mick Snow'); + entity.fullname = 'Andre Agassi'; + expect(entity.firstname).equal('Andre'); + expect(entity.lastname).equal('Agassi'); + expect(entity.entityData).deep.equal({ firstname: 'Andre', lastname: 'Agassi' }); + }); - it('should read and parse virtual (set)', () => { - model = new User({ fullname: 'John Snow' }); + it('should Not override', () => { + entity = new User({ firstname: 'John', lastname: 'Snow', fullname: 'Jooohn' }); + const entityData = entity.__getEntityDataWithVirtuals(); - const entityData = model.getEntityDataWithVirtuals(); + expect(entityData.fullname).equal('Jooohn'); + }); - expect(entityData.firstname).equal('John'); - expect(entityData.lastname).equal('Snow'); - }); + it('should read and parse virtual (set)', () => { + entity = new User({ fullname: 'John Snow' }); - it('should override existing', () => { - model = new User({ firstname: 'Peter', fullname: 'John Snow' }); + const entityData = entity.__getEntityDataWithVirtuals(); - const entityData = model.getEntityDataWithVirtuals(); + expect(entityData.firstname).equal('John'); + expect(entityData.lastname).equal('Snow'); + }); - expect(entityData.firstname).equal('John'); - }); + it('should override existing', () => { + entity = new User({ firstname: 'Peter', fullname: 'John Snow' }); - it('should not allow reserved name for virtuals', () => { - const func = () => { - schema.virtual('plain').get(function getFullName() { - return `${this.firstname} ${this.lastname}`; - }); - }; + const entityData = entity.__getEntityDataWithVirtuals(); - expect(func).throws(); - }); + expect(entityData.firstname).equal('John'); }); - describe('save()', () => { - const data = { name: 'John', lastname: 'Snow' }; - - beforeEach(() => { - entity = new GstoreModel(data); + it('should not allow reserved name for virtuals', () => { + const func = () => { + schema.virtual('plain').get(function getFullName() { + return `${this.firstname} ${this.lastname}`; }); + }; - it('should return the entity saved', () => ( - entity.save().then(_entity => { - expect(_entity.className).equal('Entity'); - }) - )); + expect(func).throws(); + }); + }); - it('should validate() before', () => { - const validateSpy = sinon.spy(entity, 'validate'); + describe('save()', () => { + const data = { name: 'John', lastname: 'Snow' }; - return entity.save().then(() => { - expect(validateSpy.called).equal(true); - }); - }); + beforeEach(() => { + entity = new GstoreModel(data); + }); - it('should NOT validate() data before', () => { - schema = new Schema({}, { validateBeforeSave: false }); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); - const validateSpy = sinon.spy(entity, 'validate'); + it('should return the entity saved', () => + entity.save().then(_entity => { + expect(_entity.__className).equal('Entity'); + })); - return entity.save().then(() => { - expect(validateSpy.called).equal(false); - }); - }); + it('should validate() before', () => { + const validateSpy = sinon.spy(entity, 'validate'); - it('should NOT save to Datastore if it didn\'t pass property validation', done => { - entity = new GstoreModel({ unknown: 'John' }); - - entity - .save(null, { sanitizeEntityData: false }) - .then(() => { - throw new Error('Should not enter here.'); - }) - .catch(err => { - assert.isDefined(err); - expect(err.message).not.equal('Should not enter here.'); - expect(ds.save.called).equal(false); - expect(err.code).equal(gstoreErrors.errorCodes.ERR_VALIDATION); - done(); - }); - }); + return entity.save().then(() => { + expect(validateSpy.called).equal(true); + }); + }); - it('should NOT save to Datastore if it didn\'t pass value validation', done => { - entity = new GstoreModel({ website: 'mydomain' }); + it('should NOT validate() data before', () => { + schema = new Schema({}, { validateBeforeSave: false }); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); + const validateSpy = sinon.spy(entity, 'validate'); - entity.save().catch(err => { - assert.isDefined(err); - expect(ds.save.called).equal(false); - done(); - }); - }); + return entity.save().then(() => { + expect(validateSpy.called).equal(false); + }); + }); - it('should convert to Datastore format before saving to Datastore', () => { - const spySerializerToDatastore = sinon.spy(datastoreSerializer, 'toDatastore'); + it("should NOT save to Datastore if it didn't pass property validation", done => { + entity = new GstoreModel({ unknown: 'John' }); + + entity + .save(null, { sanitizeEntityData: false }) + .then(() => { + throw new Error('Should not enter here.'); + }) + .catch(err => { + assert.isDefined(err); + expect(err.message).not.equal('Should not enter here.'); + expect(ds.save.called).equal(false); + expect(err.code).equal(ERROR_CODES.ERR_VALIDATION); + done(); + }); + }); - return entity.save().then(() => { - expect(entity.gstore.ds.save.calledOnce).equal(true); - expect(spySerializerToDatastore.called).equal(true); - expect(spySerializerToDatastore.getCall(0).args[0].className).equal('Entity'); - expect(spySerializerToDatastore.getCall(0).args[0].entityData).equal(entity.entityData); - expect(spySerializerToDatastore.getCall(0).args[0].excludeFromIndexes).equal(entity.excludeFromIndexes); - assert.isDefined(entity.gstore.ds.save.getCall(0).args[0].key); - expect(entity.gstore.ds.save.getCall(0).args[0].key.constructor.name).equal('Key'); - assert.isDefined(entity.gstore.ds.save.getCall(0).args[0].data); + it("should NOT save to Datastore if it didn't pass value validation", done => { + entity = new GstoreModel({ website: 'mydomain' }); - spySerializerToDatastore.restore(); - }); - }); + entity.save().catch(err => { + assert.isDefined(err); + expect(ds.save.called).equal(false); + done(); + }); + }); - it('should set "upsert" method by default', () => ( - entity.save().then(() => { - expect(entity.gstore.ds.save.getCall(0).args[0].method).equal('upsert'); - }) - )); - - describe('options', () => { - it('should accept a "method" parameter in options', () => ( - entity.save(null, { method: 'insert' }).then(() => { - expect(entity.gstore.ds.save.getCall(0).args[0].method).equal('insert'); - }) - )); - - it('should only allow "update", "insert", "upsert" as method', done => { - entity.save(null, { method: 'something' }).catch(e => { - expect(e.message).equal('Method must be either "update", "insert" or "upsert"'); - - entity.save(null, { method: 'update' }) - .then(() => entity.save(null, { method: 'upsert' })) - .then(() => { - done(); - }); - }); - }); - }); + it('should convert to Datastore format before saving to Datastore', () => { + const spySerializerToDatastore = sinon.spy(datastoreSerializer, 'toDatastore'); + + return entity.save().then(() => { + expect(entity.gstore.ds.save.calledOnce).equal(true); + expect(spySerializerToDatastore.called).equal(true); + expect(spySerializerToDatastore.getCall(0).args[0].__className).equal('Entity'); + expect(spySerializerToDatastore.getCall(0).args[0].entityData).equal(entity.entityData); + expect(spySerializerToDatastore.getCall(0).args[0].__excludeFromIndexes).equal(entity.__excludeFromIndexes); + assert.isDefined(entity.gstore.ds.save.getCall(0).args[0].key); + expect(entity.gstore.ds.save.getCall(0).args[0].key.constructor.name).equal('Key'); + assert.isDefined(entity.gstore.ds.save.getCall(0).args[0].data); + + spySerializerToDatastore.restore(); + }); + }); - it('on Datastore error, return the error', () => { - ds.save.restore(); + it('should set "upsert" method by default', () => + entity.save().then(() => { + expect(entity.gstore.ds.save.getCall(0).args[0].method).equal('upsert'); + })); - const error = { - code: 500, - message: 'Server Error', - }; - sinon.stub(ds, 'save').rejects(error); + describe('options', () => { + it('should accept a "method" parameter in options', () => + entity.save(null, { method: 'insert' }).then(() => { + expect(entity.gstore.ds.save.getCall(0).args[0].method).equal('insert'); + })); - entity = new GstoreModel({}); + it('should only allow "update", "insert", "upsert" as method', done => { + entity.save(null, { method: 'something' }).catch(e => { + expect(e.message).equal('Method must be either "update", "insert" or "upsert"'); - return entity.save().catch(err => { - expect(err).equal(error); + entity + .save(null, { method: 'update' }) + .then(() => entity.save(null, { method: 'upsert' })) + .then(() => { + done(); }); }); + }); + }); - it('should save entity in a transaction and execute "pre" hooks first', () => { - schema = new Schema({}); - const spyPreHook = sinon.spy(); - schema.pre('save', () => { - spyPreHook(); - return Promise.resolve(); - }); - - const OtherModel = gstore.model('TransactionHooks', schema, gstore); - entity = new OtherModel({}); + it('on Datastore error, return the error', () => { + ds.save.restore(); - return entity.save(transaction) - .then(_entity => { - expect(spyPreHook.called).equal(true); - expect(transaction.save.called).equal(true); - expect(spyPreHook.calledBefore(transaction.save)).equal(true); - assert.isDefined(_entity.entityData); - }); - }); + const error = { + code: 500, + message: 'Server Error', + }; + sinon.stub(ds, 'save').rejects(error); - it('should *not* save entity in a transaction if there are "pre" hooks', () => { - schema = new Schema({}); - const spyPreHook = sinon.spy(); - schema.pre('save', () => { - spyPreHook(); - return Promise.resolve(); - }); - const OtherModel = gstore.model('TransactionHooks', schema, gstore); - entity = new OtherModel({}); + entity = new GstoreModel({}); - entity.save(transaction); + return entity.save().catch(err => { + expect(err).equal(error); + }); + }); - expect(spyPreHook.called).equal(true); - expect(transaction.save.called).equal(false); - }); + it('should save entity in a transaction and execute "pre" hooks first', () => { + schema = new Schema({}); + const spyPreHook = sinon.spy(); + schema.pre('save', () => { + spyPreHook(); + return Promise.resolve(); + }); + + const OtherModel = gstore.model('TransactionHooks', schema, gstore); + entity = new OtherModel({}); + + return entity.save(transaction).then(_entity => { + expect(spyPreHook.called).equal(true); + expect(transaction.save.called).equal(true); + expect(spyPreHook.calledBefore(transaction.save)).equal(true); + assert.isDefined(_entity.entityData); + }); + }); - it('should save entity in a transaction in sync', done => { - const schema2 = new Schema({}, { validateBeforeSave: false }); - const ModelInstance2 = gstore.model('NewType', schema2, gstore); - entity = new ModelInstance2({}); - entity.save(transaction); + it('should *not* save entity in a transaction if there are "pre" hooks', () => { + schema = new Schema({}); + const spyPreHook = sinon.spy(); + schema.pre('save', () => { + spyPreHook(); + return Promise.resolve(); + }); + const OtherModel = gstore.model('TransactionHooks', schema, gstore); + entity = new OtherModel({}); - done(); - }); + entity.save(transaction); - it('should save entity in a transaction synchronous when validateBeforeSave desactivated', () => { - schema = new Schema({ name: { type: String } }, { validateBeforeSave: false }); + expect(spyPreHook.called).equal(true); + expect(transaction.save.called).equal(false); + }); - const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); - entity = new ModelInstanceTemp({}); + it('should save entity in a transaction in sync', done => { + const schema2 = new Schema({}, { validateBeforeSave: false }); + const ModelInstance2 = gstore.model('NewType', schema2, gstore); + entity = new ModelInstance2({}); + entity.save(transaction); - entity.save(transaction); - expect(transaction.save.called).equal(true); - }); + done(); + }); - it('should save entity in a transaction synchronous when disabling hook', () => { - schema = new Schema({ - name: { type: String }, - }); + it('should save entity in a transaction synchronous when validateBeforeSave desactivated', () => { + schema = new Schema({ name: { type: String } }, { validateBeforeSave: false }); - schema.pre('save', () => Promise.resolve()); + const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); + entity = new ModelInstanceTemp({}); - const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); - entity = new ModelInstanceTemp({}); - entity.preHooksEnabled = false; - entity.save(transaction); + entity.save(transaction); + expect(transaction.save.called).equal(true); + }); - const model2 = new ModelInstanceTemp({}); - const transaction2 = new Transaction(); - sinon.spy(transaction2, 'save'); - model2.save(transaction2); + it('should save entity in a transaction synchronous when disabling hook', () => { + schema = new Schema({ + name: { type: String }, + }); - expect(transaction.save.called).equal(true); - expect(transaction2.save.called).equal(false); - }); + schema.pre('save', () => Promise.resolve()); - it('should throw error if transaction not instance of Transaction', () => ( - entity.save({ id: 0 }, {}) - .catch(err => { - assert.isDefined(err); - expect(err.message).equal('Transaction needs to be a gcloud Transaction'); - }) - )); + const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); + entity = new ModelInstanceTemp({}); + entity.preHooksEnabled = false; + entity.save(transaction); - it('should call pre hooks', () => { - const spyPre = sinon.stub().resolves(); + const model2 = new ModelInstanceTemp({}); + const transaction2 = new Transaction(); + sinon.spy(transaction2, 'save'); + model2.save(transaction2); - schema = new Schema({ name: { type: String } }); - schema.pre('save', () => spyPre()); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); + expect(transaction.save.called).equal(true); + expect(transaction2.save.called).equal(false); + }); - return entity.save().then(() => { - expect(spyPre.calledBefore(ds.save)).equal(true); - }); - }); + it('should throw error if transaction not instance of Transaction', () => + entity.save({ id: 0 }, {}).catch(err => { + assert.isDefined(err); + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); - it('should call post hooks', () => { - const spyPost = sinon.stub().resolves(123); - schema = new Schema({ name: { type: String } }); - schema.post('save', () => spyPost()); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); + it('should call pre hooks', () => { + const spyPre = sinon.stub().resolves(); - return entity.save().then(result => { - expect(spyPost.called).equal(true); - expect(result.name).equal('John'); - }); - }); + schema = new Schema({ name: { type: String } }); + schema.pre('save', () => spyPre()); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); - it('error in post hooks should be added to response', () => { - const error = { code: 500 }; - const spyPost = sinon.stub().rejects(error); - schema = new Schema({ name: { type: String } }); - schema.post('save', spyPost); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); - - return entity.save().then(_entity => { - assert.isDefined(_entity[gstore.ERR_HOOKS]); - expect(_entity[gstore.ERR_HOOKS][0]).equal(error); - }); - }); + return entity.save().then(() => { + expect(spyPre.calledBefore(ds.save)).equal(true); + }); + }); - it('transaction.execPostHooks() should call post hooks', () => { - const spyPost = sinon.stub().resolves(123); - schema = new Schema({ name: { type: String } }); - schema.post('save', spyPost); + it('should call post hooks', () => { + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: String } }); + schema.post('save', () => spyPost()); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); + + return entity.save().then(result => { + expect(spyPost.called).equal(true); + expect(result.name).equal('John'); + }); + }); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); + it('error in post hooks should be added to response', () => { + const error = { code: 500 }; + const spyPost = sinon.stub().rejects(error); + schema = new Schema({ name: { type: String } }); + schema.post('save', spyPost); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); + + return entity.save().then(_entity => { + assert.isDefined(_entity[gstore.ERR_HOOKS]); + expect(_entity[gstore.ERR_HOOKS][0]).equal(error); + }); + }); - return entity.save(transaction) - .then(() => transaction.execPostHooks()) - .then(() => { - expect(spyPost.called).equal(true); - expect(spyPost.callCount).equal(1); - }); - }); + it('transaction.execPostHooks() should call post hooks', () => { + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: String } }); + schema.post('save', spyPost); - it('transaction.execPostHooks() should set scope to entity saved', done => { - schema.post('save', function preSave() { - expect(this instanceof Entity).equal(true); - expect(this.name).equal('John Jagger'); - done(); - }); - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John Jagger' }); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); - entity.save(transaction) - .then(() => transaction.execPostHooks()); + return entity + .save(transaction) + .then(() => transaction.execPostHooks()) + .then(() => { + expect(spyPost.called).equal(true); + expect(spyPost.callCount).equal(1); }); + }); - it('if transaction.execPostHooks() is NOT called post middleware should not be called', () => { - const spyPost = sinon.stub().resolves(123); - schema = new Schema({ name: { type: String } }); - schema.post('save', spyPost); - - GstoreModel = gstore.model('Blog', schema); - entity = new GstoreModel({ name: 'John' }); + it('transaction.execPostHooks() should set scope to entity saved', done => { + schema.post('save', function preSave() { + expect(this instanceof Entity.default).equal(true); + expect(this.name).equal('John Jagger'); + done(); + }); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John Jagger' }); - return entity.save(transaction) - .then(() => { - expect(spyPost.called).equal(false); - }); - }); + entity.save(transaction).then(() => transaction.execPostHooks()); + }); - it('should update modifiedOn to new Date if property in Schema', () => { - schema = new Schema({ modifiedOn: { type: Date } }); - GstoreModel = gstore.model('BlogPost', schema); - entity = new GstoreModel({}); + it('if transaction.execPostHooks() is NOT called post middleware should not be called', () => { + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: String } }); + schema.post('save', spyPost); - return entity.save().then(() => { - assert.isDefined(entity.entityData.modifiedOn); - const diff = Math.abs(entity.entityData.modifiedOn.getTime() - Date.now()); - expect(diff < 10).equal(true); - }); - }); + GstoreModel = gstore.model('Blog', schema); + entity = new GstoreModel({ name: 'John' }); - it('should convert plain geo object (latitude, longitude) to datastore GeoPoint', () => { - schema = new Schema({ location: { type: Schema.Types.GeoPoint } }); - GstoreModel = gstore.model('Car', schema); - entity = new GstoreModel({ - location: { - latitude: 37.305885314941406, - longitude: -89.51815032958984, - }, - }); + return entity.save(transaction).then(() => { + expect(spyPost.called).equal(false); + }); + }); - return entity.save().then(() => { - expect(entity.entityData.location.constructor.name).to.equal('GeoPoint'); - }); - }); + it('should update modifiedOn to new Date if property in Schema', () => { + schema = new Schema({ modifiedOn: { type: Date } }); + GstoreModel = gstore.model('BlogPost', schema); + entity = new GstoreModel({}); - it('should sanitize the entityData', () => { - schema = new Schema({ name: { type: String } }); - GstoreModel = gstore.model('TestValidate', schema); - entity = new GstoreModel({ name: 'John', unknown: 'abc' }); + return entity.save().then(() => { + assert.isDefined(entity.entityData.modifiedOn); + const diff = Math.abs(entity.entityData.modifiedOn.getTime() - Date.now()); + expect(diff < 10).equal(true); + }); + }); - return entity.save().then(() => { - assert.isUndefined(entity.entityData.unknown); - }); - }); + it('should convert plain geo object (latitude, longitude) to datastore GeoPoint', () => { + schema = new Schema({ location: { type: Schema.Types.GeoPoint } }); + GstoreModel = gstore.model('Car', schema); + entity = new GstoreModel({ + location: { + latitude: 37.305885314941406, + longitude: -89.51815032958984, + }, + }); + + return entity.save().then(() => { + expect(entity.entityData.location.constructor.name).to.equal('GeoPoint'); + }); + }); - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); + it('should sanitize the entityData', () => { + schema = new Schema({ name: { type: String } }); + GstoreModel = gstore.model('TestValidate', schema); + entity = new GstoreModel({ name: 'John', unknown: 'abc' }); - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should call GstoreModel.clearCache()', () => { - sinon.spy(GstoreModel, 'clearCache'); - return entity.save().then(_entity => { - assert.ok(GstoreModel.clearCache.called); - expect(typeof GstoreModel.clearCache.getCall(0).args[0]).equal('undefined'); - expect(_entity.name).equal('John'); - GstoreModel.clearCache.restore(); - }); - }); + return entity.save().then(() => { + assert.isUndefined(entity.entityData.unknown); + }); + }); - it('on error when clearing the cache, should add the entity saved on the error object', done => { - const err = new Error('Houston something bad happened'); - sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - - entity.save() - .catch(e => { - expect(e.__entity.name).equal('John'); - expect(e.__cacheError).equal(err); - gstore.cache.queries.clearQueriesByKind.restore(); - done(); - }); - }); - }); + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should call GstoreModel.clearCache()', () => { + sinon.spy(GstoreModel, 'clearCache'); + return entity.save().then(_entity => { + assert.ok(GstoreModel.clearCache.called); + expect(typeof GstoreModel.clearCache.getCall(0).args[0]).equal('undefined'); + expect(_entity.name).equal('John'); + GstoreModel.clearCache.restore(); + }); + }); + + it('on error when clearing the cache, should add the entity saved on the error object', done => { + const err = new Error('Houston something bad happened'); + sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); + + entity.save().catch(e => { + expect(e.__entity.name).equal('John'); + expect(e.__cacheError).equal(err); + gstore.cache.queries.clearQueriesByKind.restore(); + done(); + }); + }); }); + }); - describe('validate()', () => { - beforeEach(() => { - sinon.spy(validation, 'validate'); - }); + describe('validate()', () => { + beforeEach(() => { + sinon.spy(validation, 'validate'); + }); - afterEach(() => { - validation.validate.restore(); - }); + afterEach(() => { + validation.validate.restore(); + }); - it('should call "Validation" helper passing entityData, Schema & entityKind', () => { - schema = new Schema({ name: { type: String } }); - GstoreModel = gstore.model('TestValidate', schema); - entity = new GstoreModel({ name: 'John' }); + it('should call "Validation" helper passing entityData, Schema & entityKind', () => { + schema = new Schema({ name: { type: String } }); + GstoreModel = gstore.model('TestValidate', schema); + entity = new GstoreModel({ name: 'John' }); - const { error } = entity.validate(); + const { error } = entity.validate(); - assert.isDefined(error); - expect(validation.validate.getCall(0).args[0]).deep.equal(entity.entityData); - expect(validation.validate.getCall(0).args[1]).equal(schema); - expect(validation.validate.getCall(0).args[2]).equal(entity.entityKind); - }); + assert.isDefined(error); + expect(validation.validate.getCall(0).args[0]).deep.equal(entity.entityData); + expect(validation.validate.getCall(0).args[1]).equal(schema); + expect(validation.validate.getCall(0).args[2]).equal(entity.entityKind); + }); - it('should maintain the Datastore Key on the entityData with Joi Schema', () => { - schema = new Schema({ name: { joi: Joi.string() } }, { joi: true }); - GstoreModel = gstore.model('TestValidate3', schema); - entity = new GstoreModel({ name: 'John', createdOn: 'abc' }); - const key = entity.entityData[gstore.ds.KEY]; + it('should maintain the Datastore Key on the entityData with Joi Schema', () => { + schema = new Schema({ name: { joi: Joi.string() } }, { joi: true }); + GstoreModel = gstore.model('TestValidate3', schema); + entity = new GstoreModel({ name: 'John', createdOn: 'abc' }); + const key = entity.entityData[gstore.ds.KEY]; - entity.validate(); + entity.validate(); - expect(entity.entityData[gstore.ds.KEY]).equal(key); - }); + expect(entity.entityData[gstore.ds.KEY]).equal(key); }); + }); }); diff --git a/test/error-test.js b/test/error-test.js index 7128670..08075f5 100755 --- a/test/error-test.js +++ b/test/error-test.js @@ -1,4 +1,3 @@ - 'use strict'; const util = require('util'); @@ -9,158 +8,74 @@ const { GstoreError, TypeError, message } = errors; const { expect, assert } = chai; const doSomethingBad = code => { - code = code || 'ERR_GENERIC'; - throw new GstoreError(code); + code = code || 'ERR_GENERIC'; + throw new GstoreError(code); }; describe('message()', () => { - it('should return string passed', () => { - expect(message('My message')).equal('My message'); - }); - - it('should return string passed with arguments', () => { - expect(message('Hello %s %s', 'John', 'Snow')).equal('Hello John Snow'); - expect(message('Age: %d years old', 27)).equal('Age: 27 years old'); - }); + it('should return string passed', () => { + expect(message('My message')).equal('My message'); + }); + + it('should return string passed with arguments', () => { + expect(message('Hello %s %s', 'John', 'Snow')).equal('Hello John Snow'); + expect(message('Age: %d years old', 27)).equal('Age: 27 years old'); + }); }); describe('GstoreError', () => { - it('should create a custom Error', () => { - try { - doSomethingBad(); - } catch (e) { - expect(e.name).equal('GstoreError'); - expect(e instanceof GstoreError); - expect(e instanceof Error); - - // The error should be recognized by Node.js' util#isError - expect(util.isError(e)).equal(true); - assert.isDefined(e.stack); - expect(e.toString()).equal('GstoreError: An error occured'); - - // The stack should start with the default error message formatting - expect(e.stack.split('\n')[0]).equal('GstoreError: An error occured'); - - // The first stack frame should be the function where the error was thrown. - expect(e.stack.split('\n')[1].indexOf('doSomethingBad')).equal(7); - - // The error code should be set - expect(e.code).equal('ERR_GENERIC'); - } - }); - - it('should fall back to generic if no message passed', () => { - const func = () => { - throw new GstoreError(); - }; - - try { - func(); - } catch (e) { - expect(e.code).equal('ERR_GENERIC'); - expect(e.toString()).equal('GstoreError: An error occured'); - } - }); - - it('should have static errors', () => { - assert.isDefined(GstoreError.TypeError); - assert.isDefined(GstoreError.ValueError); - assert.isDefined(GstoreError.ValidationError); - }); + it('should create a custom Error', () => { + try { + doSomethingBad(); + } catch (e) { + expect(e.name).equal('GstoreError'); + expect(e instanceof GstoreError); + expect(e instanceof Error); + + // The error should be recognized by Node.js' util#isError + expect(util.isError(e)).equal(true); + assert.isDefined(e.stack); + expect(e.toString()).equal('GstoreError: An error occured'); + + // The stack should start with the default error message formatting + expect(e.stack.split('\n')[0]).equal('GstoreError: An error occured'); + + // The first stack frame should be the function where the error was thrown. + expect(e.stack.split('\n')[1].indexOf('doSomethingBad')).equal(7); + + // The error code should be set + expect(e.code).equal('ERR_GENERIC'); + } + }); + + it('should fall back to generic if no message passed', () => { + const func = () => { + throw new GstoreError(); + }; + + try { + func(); + } catch (e) { + expect(e.code).equal('ERR_GENERIC'); + expect(e.toString()).equal('GstoreError: An error occured'); + } + }); }); describe('TypeError', () => { - it('should create a TypeError', () => { - const throwTypeError = code => { - code = code || 'ERR_GENERIC'; - throw new TypeError(code); - }; - - try { - throwTypeError(); - } catch (e) { - expect(e.name).equal('TypeError'); - expect(e instanceof TypeError); - expect(e instanceof GstoreError); - expect(e instanceof Error); - - // The error should be recognized by Node.js' util#isError - // expect(util.isError(e)).equal(true); - // assert.isDefined(e.stack); - // expect(e.toString()).equal('GstoreError: An error occured'); - - // // The stack should start with the default error message formatting - // expect(e.stack.split('\n')[0]).equal('GstoreError: An error occured'); - - // // The first stack frame should be the function where the error was thrown. - // expect(e.stack.split('\n')[1].indexOf('doSomethingBad')).equal(7); - - // // The error code should be set - // expect(e.code).equal('ERR_GENERIC'); - } - }); - - // it('should extend Error', () => { - // expect(ValidationError.prototype.name).equal('Error'); - // }); - - // it('should return error data passed in param', () => { - // const errorData = { - // code: 400, - // message: 'Something went really bad', - // }; - // const error = new ValidationError(errorData); - - // expect(error.message).equal(errorData); - // }); - - // it('should return "{entityKind} validation failed" if called with entity instance', () => { - // const entityKind = 'Blog'; - // const schema = new Schema({}); - // const ModelInstance = Model.compile(entityKind, schema, gstore); - // const model = new ModelInstance({}); - // const error = new ValidationError(model); - - // expect(error.message).equal(`${entityKind} validation failed`); - // }); - - // it('should return "Validation failed" if called without param', () => { - // const error = new ValidationError(); - - // expect(error.message).equal('Validation failed'); - // }); -}); - -describe('ValidatorError', () => { - // it('should extend Error', () => { - // expect(ValidatorError.prototype.name).equal('Error'); - // }); - - // it('should return error data passed in param', () => { - // const errorData = { - // code: 400, - // message: 'Something went really bad', - // }; - // const error = new ValidatorError(errorData); - - // expect(error.message.errorName).equal('Wrong format'); - // expect(error.message.message).equal(errorData.message); - // }); - - // it('should set error name passed in param', () => { - // const errorData = { - // code: 400, - // errorName: 'Required', - // message: 'Something went really bad', - // }; - // const error = new ValidatorError(errorData); - - // expect(error.message.errorName).equal(errorData.errorName); - // }); - - // it('should return "Validation failed" if called without param', () => { - // const error = new ValidatorError(); - - // expect(error.message).equal('Value validation failed'); - // }); + it('should create a TypeError', () => { + const throwTypeError = code => { + code = code || 'ERR_GENERIC'; + throw new TypeError(code); + }; + + try { + throwTypeError(); + } catch (e) { + expect(e.name).equal('TypeError'); + expect(e instanceof TypeError); + expect(e instanceof GstoreError); + expect(e instanceof Error); + } + }); }); diff --git a/test/helpers/defaultValues.js b/test/helpers/defaultValues.js index 8575f28..d83aa4d 100644 --- a/test/helpers/defaultValues.js +++ b/test/helpers/defaultValues.js @@ -1,29 +1,29 @@ 'use strict'; const chai = require('chai'); -const defaultValues = require('../../lib/helpers/defaultValues'); +const { default: defaultValues } = require('../../lib/helpers/defaultValues'); const { expect } = chai; describe('Query Helpers', () => { - describe('defaultValues constants handler()', () => { - it('should return the current time', () => { - const value = defaultValues.NOW; - const result = defaultValues.__handler__(value); + describe('defaultValues constants handler()', () => { + it('should return the current time', () => { + const value = defaultValues.NOW; + const result = defaultValues.__handler__(value); - /** - * we might have a slightly difference, that's ok :) - */ - const dif = Math.abs(result.getTime() - new Date().getTime()); + /** + * we might have a slightly difference, that's ok :) + */ + const dif = Math.abs(result.getTime() - new Date().getTime()); - expect(dif).to.be.below(100); - }); + expect(dif).to.be.below(100); + }); - it('should return null if value passed not in map', () => { - const value = 'DOES_NOT_EXIST'; - const result = defaultValues.__handler__(value); + it('should return null if value passed not in map', () => { + const value = 'DOES_NOT_EXIST'; + const result = defaultValues.__handler__(value); - expect(result).equal(null); - }); + expect(result).equal(null); }); + }); }); diff --git a/test/helpers/queryhelpers-test.js b/test/helpers/queryhelpers-test.js index db276c7..7f5faae 100644 --- a/test/helpers/queryhelpers-test.js +++ b/test/helpers/queryhelpers-test.js @@ -5,163 +5,169 @@ const sinon = require('sinon'); const { Datastore } = require('@google-cloud/datastore'); const ds = new Datastore(); -const { queryHelpers } = require('../../lib/helpers'); +const { default: helpers } = require('../../lib/helpers'); + +const { queryHelpers } = helpers; const { expect } = chai; describe('Query Helpers', () => { - let query; + let query; + + describe('should build a Query from options', () => { + beforeEach(() => { + query = ds.createQuery(); + }); + + it('and throw error if no query passed', () => { + const fn = () => { + queryHelpers.buildQueryFromOptions(); + }; + + expect(fn).to.throw(Error); + }); - describe('should build a Query from options', () => { - beforeEach(() => { - query = ds.createQuery(); - }); + it('and throw error if query is not a gcloud Query', () => { + const fn = () => { + queryHelpers.buildQueryFromOptions({}); + }; - it('and throw error if no query passed', () => { - const fn = () => { queryHelpers.buildFromOptions(); }; + expect(fn).to.throw(Error); + }); - expect(fn).to.throw(Error); - }); + it('and not modify query if no options passed', () => { + const originalQuery = {}; + Object.keys(query).forEach(k => { + originalQuery[k] = query[k]; + }); - it('and throw error if query is not a gcloud Query', () => { - const fn = () => { queryHelpers.buildFromOptions({}); }; + query = queryHelpers.buildQueryFromOptions(query); - expect(fn).to.throw(Error); - }); + expect(query.filters).deep.equal(originalQuery.filters); + expect(query.limitVal).equal(originalQuery.limitVal); + expect(query.orders).deep.equal(originalQuery.orders); + expect(query.selectVal).deep.equal(originalQuery.selectVal); + }); + + it('and update query', () => { + const options = { + limit: 10, + order: { property: 'name', descending: true }, + filters: [], + select: 'name', + }; + + query = queryHelpers.buildQueryFromOptions(query, options); + + expect(query.limitVal).equal(options.limit); + expect(query.orders.length).equal(1); + expect(query.orders[0].name).equal('name'); + expect(query.orders[0].sign).equal('-'); + expect(query.selectVal).deep.equal(['name']); + }); - it('and not modify query if no options passed', () => { - const originalQuery = {}; - Object.keys(query).forEach(k => { - originalQuery[k] = query[k]; - }); - - query = queryHelpers.buildFromOptions(query); + it('and allow order on serveral properties', () => { + const options = { + order: [{ property: 'name', descending: true }, { property: 'age' }], + }; - expect(query.filters).deep.equal(originalQuery.filters); - expect(query.limitVal).equal(originalQuery.limitVal); - expect(query.orders).deep.equal(originalQuery.orders); - expect(query.selectVal).deep.equal(originalQuery.selectVal); - }); + query = queryHelpers.buildQueryFromOptions(query, options); - it('and update query', () => { - const options = { - limit: 10, - order: { property: 'name', descending: true }, - filters: [], - select: 'name', - }; - - query = queryHelpers.buildFromOptions(query, options); - - expect(query.limitVal).equal(options.limit); - expect(query.orders.length).equal(1); - expect(query.orders[0].name).equal('name'); - expect(query.orders[0].sign).equal('-'); - expect(query.selectVal).deep.equal(['name']); - }); - - it('and allow order on serveral properties', () => { - const options = { - order: [{ property: 'name', descending: true }, { property: 'age' }], - }; - - query = queryHelpers.buildFromOptions(query, options); - - expect(query.orders.length).equal(2); - }); - - it('and allow select to be an Array', () => { - const options = { - select: ['name', 'lastname', 'email'], - }; - - query = queryHelpers.buildFromOptions(query, options, ds); - - expect(query.selectVal).deep.equal(options.select); - }); - - it('and update hasAncestor in query', () => { - const options = { - ancestors: ['Parent', 1234], - }; - - query = queryHelpers.buildFromOptions(query, options, ds); - - expect(query.filters[0].op).equal('HAS_ANCESTOR'); - expect(query.filters[0].val.kind).equal('Parent'); - expect(query.filters[0].val.id).equal(1234); - }); - - it('and throw Error if no Datastore instance passed when passing ancestors', () => { - const options = { - ancestors: ['Parent', 123], - }; - - const fn = () => { - query = queryHelpers.buildFromOptions(query, options); - }; - - expect(fn).to.throw(Error); - }); - - it('and define one filter', () => { - const options = { - filters: ['name', '=', 'John'], - }; - - query = queryHelpers.buildFromOptions(query, options, ds); - - expect(query.filters.length).equal(1); - expect(query.filters[0].name).equal('name'); - expect(query.filters[0].op).equal('='); - expect(query.filters[0].val).equal('John'); - }); + expect(query.orders.length).equal(2); + }); + + it('and allow select to be an Array', () => { + const options = { + select: ['name', 'lastname', 'email'], + }; + + query = queryHelpers.buildQueryFromOptions(query, options, ds); + + expect(query.selectVal).deep.equal(options.select); + }); + + it('and update hasAncestor in query', () => { + const options = { + ancestors: ['Parent', 1234], + }; + + query = queryHelpers.buildQueryFromOptions(query, options, ds); + + expect(query.filters[0].op).equal('HAS_ANCESTOR'); + expect(query.filters[0].val.kind).equal('Parent'); + expect(query.filters[0].val.id).equal(1234); + }); - it('and define several filters', () => { - const options = { - filters: [['name', '=', 'John'], ['lastname', 'Snow'], ['age', '<', 30]], - }; - - query = queryHelpers.buildFromOptions(query, options, ds); - - expect(query.filters.length).equal(3); - expect(query.filters[1].name).equal('lastname'); - expect(query.filters[1].op).equal('='); - expect(query.filters[1].val).equal('Snow'); - expect(query.filters[2].op).equal('<'); - }); - - it('and execute a function in a filter value, without modifying the filters Array', () => { - const spy = sinon.spy(); - const options = { - filters: [['modifiedOn', '<', spy]], - }; - - query = queryHelpers.buildFromOptions(query, options, ds); - - expect(spy.calledOnce).equal(true); - expect(options.filters[0][2]).to.equal(spy); - }); - - it('and throw error if wrong format for filters', () => { - const options = { - filters: 'name', - }; - const fn = () => { - query = queryHelpers.buildFromOptions(query, options, ds); - }; + it('and throw Error if no Datastore instance passed when passing ancestors', () => { + const options = { + ancestors: ['Parent', 123], + }; + + const fn = () => { + query = queryHelpers.buildQueryFromOptions(query, options); + }; + + expect(fn).to.throw(Error); + }); + + it('and define one filter', () => { + const options = { + filters: ['name', '=', 'John'], + }; + + query = queryHelpers.buildQueryFromOptions(query, options, ds); + + expect(query.filters.length).equal(1); + expect(query.filters[0].name).equal('name'); + expect(query.filters[0].op).equal('='); + expect(query.filters[0].val).equal('John'); + }); + + it('and define several filters', () => { + const options = { + filters: [['name', '=', 'John'], ['lastname', 'Snow'], ['age', '<', 30]], + }; + + query = queryHelpers.buildQueryFromOptions(query, options, ds); + + expect(query.filters.length).equal(3); + expect(query.filters[1].name).equal('lastname'); + expect(query.filters[1].op).equal('='); + expect(query.filters[1].val).equal('Snow'); + expect(query.filters[2].op).equal('<'); + }); + + it('and execute a function in a filter value, without modifying the filters Array', () => { + const spy = sinon.spy(); + const options = { + filters: [['modifiedOn', '<', spy]], + }; + + query = queryHelpers.buildQueryFromOptions(query, options, ds); + + expect(spy.calledOnce).equal(true); + expect(options.filters[0][2]).to.equal(spy); + }); + + it('and throw error if wrong format for filters', () => { + const options = { + filters: 'name', + }; + const fn = () => { + query = queryHelpers.buildQueryFromOptions(query, options, ds); + }; + + expect(fn).to.throw(Error); + }); - expect(fn).to.throw(Error); - }); - - it('and add start cursor', () => { - const options = { - start: 'abcdef', - }; + it('and add start cursor', () => { + const options = { + start: 'abcdef', + }; - query = queryHelpers.buildFromOptions(query, options, ds); + query = queryHelpers.buildQueryFromOptions(query, options, ds); - expect(query.startVal).equal(options.start); - }); + expect(query.startVal).equal(options.start); }); + }); }); diff --git a/test/helpers/validation-test.js b/test/helpers/validation-test.js index b5b65c8..9b11e3c 100755 --- a/test/helpers/validation-test.js +++ b/test/helpers/validation-test.js @@ -3,24 +3,25 @@ const chai = require('chai'); const Joi = require('@hapi/joi'); -const Schema = require('../../lib/schema'); -const gstoreErrors = require('../../lib/errors'); -const { validation } = require('../../lib/helpers'); +const { default: Schema } = require('../../lib/schema'); +const { ERROR_CODES } = require('../../lib/errors'); +const { default: helpers } = require('../../lib/helpers'); + +const { validation } = helpers; const ds = require('../mocks/datastore')({ - namespace: 'com.mydomain', + namespace: 'com.mydomain', }); const { expect, assert } = chai; -const { errorCodes } = gstoreErrors; const customValidationFunction = (obj, validator, min, max) => { - if ('embeddedEntity' in obj) { - const { value } = obj.embeddedEntity; - return validator.isNumeric(value.toString()) && (value >= min) && (value <= max); - } + if ('embeddedEntity' in obj) { + const { value } = obj.embeddedEntity; + return validator.isNumeric(value.toString()) && value >= min && value <= max; + } - return false; + return false; }; /** @@ -29,512 +30,527 @@ const customValidationFunction = (obj, validator, min, max) => { * Once they will be deprecated we can delete the Validation (old Types) below. */ describe('Validation', () => { - let schema; - - const validate = entityData => ( - validation.validate(entityData, schema, 'MyEntityKind', ds) - ); - - beforeEach(() => { - schema = new Schema({ - name: { type: String }, - lastname: { type: String }, - age: { type: Number }, - birthday: { type: Date }, - street: {}, - website: { validate: 'isURL' }, - email: { validate: 'isEmail' }, - ip: { validate: { rule: 'isIP', args: [4] } }, - ip2: { validate: { rule: 'isIP' } }, // no args passed - modified: { type: Boolean }, - tags: { type: Array }, - prefs: { type: Object }, - price: { type: Schema.Types.Double }, - icon: { type: Buffer }, - location: { type: Schema.Types.GeoPoint }, - color: { validate: 'isHexColor' }, - type: { values: ['image', 'video'] }, - customFieldWithEmbeddedEntity: { - type: Object, - validate: { - rule: customValidationFunction, - args: [4, 10], - }, - }, - company: { type: Schema.Types.Key }, - address: { type: Schema.Types.Key, ref: 'Address' }, - }); - - schema.virtual('fullname').get(() => { }); - }); - - it('should return an object with an "error" and "value" properties', () => { - const entityData = { name: 'John' }; - - const { error, value } = validate(entityData); - - assert.isDefined(error); + let schema; + + const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind', ds); + + beforeEach(() => { + schema = new Schema({ + name: { type: String }, + lastname: { type: String }, + age: { type: Number }, + birthday: { type: Date }, + street: {}, + website: { validate: 'isURL' }, + email: { validate: 'isEmail' }, + ip: { validate: { rule: 'isIP', args: [4] } }, + ip2: { validate: { rule: 'isIP' } }, // no args passed + modified: { type: Boolean }, + tags: { type: Array }, + prefs: { type: Object }, + price: { type: Schema.Types.Double }, + icon: { type: Buffer }, + location: { type: Schema.Types.GeoPoint }, + color: { validate: 'isHexColor' }, + type: { values: ['image', 'video'] }, + customFieldWithEmbeddedEntity: { + type: Object, + validate: { + rule: customValidationFunction, + args: [4, 10], + }, + }, + company: { type: Schema.Types.Key }, + address: { type: Schema.Types.Key, ref: 'Address' }, + }); + + schema.virtual('fullname').get(() => {}); + }); + + it('should return an object with an "error" and "value" properties', () => { + const entityData = { name: 'John' }; + + const { error, value } = validate(entityData); + + assert.isDefined(error); + expect(value).equal(entityData); + }); + + it('should return a Promise and resolve with the entityData', () => { + const entityData = { name: 'John' }; + + return validate(entityData) + .then(value => { expect(value).equal(entityData); - }); - - it('should return a Promise and resolve with the entityData', () => { - const entityData = { name: 'John' }; - - return validate(entityData).then(value => { - expect(value).equal(entityData); - return Promise.resolve('test'); - }) - .catch(() => { }) - .then(response => { - expect(response).equal('test'); - }); - }); - - it('should return a Promise and reject with the error', () => { - const entityData = { name: 123 }; - - return validate(entityData).then(() => { - }, error => { - expect(error.name).equal('ValidationError'); - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - }); - - it('should return a Promise catch with the error', () => { - const entityData = { name: 123 }; - - return validate(entityData).catch(error => { - expect(error.name).equal('ValidationError'); - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - return Promise.resolve('test'); - }).then(response => { - // Just to make sure we can chain Promises - expect(response).equal('test'); - }); - }); - - it('properties passed ok', () => { - const { error } = validate({ name: 'John', lastname: 'Snow' }); - - expect(error).equal(null); - }); - - it('properties passed ko', () => { - const { error } = validate({ unknown: 123 }); - - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_NOT_ALLOWED); - }); - - it('should remove virtuals before validating', () => { - const { error } = validate({ fullname: 'John Snow' }); - - expect(error).equal(null); - }); - - it('accept unkwown properties when "explicityOnly" set to false', () => { - schema = new Schema({ name: { type: 'string' } }, { explicitOnly: false }); - - const { error } = validate({ unknown: 123 }); - - expect(error).equal(null); - }); - - it('required property', () => { - schema = new Schema({ - name: { type: 'string' }, - email: { type: 'string', required: true }, - }); - - const { error } = validate({ name: 'John Snow', email: '' }); - const { error: error2 } = validate({ name: 'John Snow', email: ' ' }); - const { error: error3 } = validate({ name: 'John Snow', email: null }); - - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); - expect(error2.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); - expect(error3.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); - }); - - it('don\'t validate empty value', () => { - const { error } = validate({ email: undefined }); - const { error: error2 } = validate({ email: null }); - const { error: error3 } = validate({ email: '' }); - - expect(error).equal(null); - expect(error2).equal(null); - expect(error3).equal(null); - }); - - it('no type validation', () => { - const { error } = validate({ street: 123 }); - const { error: error2 } = validate({ street: '123' }); - const { error: error3 } = validate({ street: true }); - - expect(error).equal(null); - expect(error2).equal(null); - expect(error3).equal(null); - }); - - it('--> Datstore Key ok', () => { - const company = ds.key(['EntityKind', 123]); - const { error } = validate({ company }); - - expect(error).equal(null); - }); - - it('--> Datstore Key ko', () => { - const { error } = validate({ company: 123 }); - - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); - expect(error.errors[0].ref).equal('key.base'); - }); - - it('--> Datstore Key ko', () => { - const address = ds.key(['WrongReference', 123]); - const { error } = validate({ address }); - - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); - expect(error.errors[0].ref).equal('key.entityKind'); - }); - - it('--> string', () => { - const { error } = validate({ name: 123 }); - - expect(error).not.equal(null); - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); - }); - - it('--> number', () => { - const { error } = validate({ age: 'string' }); - - expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); - }); - - it('--> int', () => { - const { error } = validate({ age: ds.int('7') }); - const { error: error2 } = validate({ age: ds.int(7) }); - const { error: error3 } = validate({ age: 7 }); - const { error: error4 } = validate({ age: ds.int('string') }); - const { error: error5 } = validate({ age: 'string' }); - const { error: error6 } = validate({ age: '7' }); - - expect(error).equal(null); - expect(error2).equal(null); - expect(error3).equal(null); - expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> double', () => { - const { error } = validate({ price: ds.double('1.2') }); - const { error: error2 } = validate({ price: ds.double(7.0) }); - const { error: error3 } = validate({ price: 7 }); - const { error: error4 } = validate({ price: 7.59 }); - const { error: error5 } = validate({ price: ds.double('str') }); - const { error: error6 } = validate({ price: 'string' }); - const { error: error7 } = validate({ price: '7' }); - - expect(error).equal(null); - expect(error2).equal(null); - expect(error3).equal(null); - expect(error4).equal(null); - expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error7.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> buffer', () => { - const { error } = validate({ icon: Buffer.from('\uD83C\uDF69') }); - const { error: error2 } = validate({ icon: 'string' }); - - expect(error).equal(null); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> boolean', () => { - const { error } = validate({ modified: true }); - const { error: error2 } = validate({ modified: 'string' }); - - expect(error).equal(null); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> object', () => { - const { error } = validate({ prefs: { check: true } }); - const { error: error2 } = validate({ prefs: 'string' }); - const { error: error3 } = validate({ prefs: [123] }); - - expect(error).equal(null); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> geoPoint', () => { - // datastore geoPoint - const { error } = validate({ - location: ds.geoPoint({ - latitude: 40.6894, - longitude: -74.0447, - }), - }); - - // valid geo object - const { error: error2 } = validate({ - location: { - latitude: 40.68942342541, - longitude: -74.044743654572, - }, - }); - - const { error: error3 } = validate({ location: 'string' }); - const { error: error4 } = validate({ location: true }); - const { error: error5 } = validate({ location: { longitude: 999, latitude: 'abc' } }); - const { error: error6 } = validate({ location: { longitude: 40.6895 } }); - const { error: error7 } = validate({ location: { longitude: '120.123', latitude: '40.12345678' } }); - - expect(error).equal(null); - expect(error2).equal(null); - expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error7.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> array ok', () => { - const { error } = validate({ tags: [] }); - - expect(error).equal(null); - }); - - it('--> array ko', () => { - const { error } = validate({ tags: {} }); - const { error: error2 } = validate({ tags: 'string' }); - const { error: error3 } = validate({ tags: 123 }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> date ok', () => { - const { error } = validate({ birthday: '2015-01-01' }); - const { error: error2 } = validate({ birthday: new Date() }); - - expect(error).equal(null); - expect(error2).equal(null); - }); - - it('--> date ko', () => { - const { error } = validate({ birthday: '01-2015-01' }); - const { error: error2 } = validate({ birthday: '01-01-2015' }); - const { error: error3 } = validate({ birthday: '2015/01/01' }); - const { error: error4 } = validate({ birthday: '01/01/2015' }); - const { error: error5 } = validate({ birthday: 12345 }); // No number allowed - const { error: error6 } = validate({ birthday: 'string' }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); - }); - - it('--> isURL ok', () => { - const { error } = validate({ website: 'http://google.com' }); - const { error: error2 } = validate({ website: 'google.com' }); - - expect(error).equal(null); - expect(error2).equal(null); - }); - - it('--> isURL ko', () => { - const { error } = validate({ website: 'domain.k' }); - const { error: error2 } = validate({ website: 123 }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - }); - - it('--> isEmail ok', () => { - const { error } = validate({ email: 'john@snow.com' }); - - expect(error).equal(null); - }); - - it('--> isEmail ko', () => { - const { error } = validate({ email: 'john@snow' }); - const { error: error2 } = validate({ email: 'john@snow.' }); - const { error: error3 } = validate({ email: 'john@snow.k' }); - const { error: error4 } = validate({ email: 'johnsnow.com' }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - }); - - it('--> is IP ok', () => { - const { error } = validate({ ip: '127.0.0.1' }); - const { error: error2 } = validate({ ip2: '127.0.0.1' }); - - expect(error).equal(null); - expect(error2).equal(null); - }); + return Promise.resolve('test'); + }) + .catch(() => {}) + .then(response => { + expect(response).equal('test'); + }); + }); + + it('should return a Promise and reject with the error', () => { + const entityData = { name: 123 }; + + return validate(entityData).then( + () => {}, + error => { + expect(error.name).equal('ValidationError'); + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }, + ); + }); - it('--> is IP ko', () => { - const { error } = validate({ ip: 'fe80::1c2e:f014:10d8:50f5' }); - const { error: error2 } = validate({ ip: '1.1.1' }); + it('should return a Promise catch with the error', () => { + const entityData = { name: 123 }; - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - }); + return validate(entityData) + .catch(error => { + expect(error.name).equal('ValidationError'); + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + return Promise.resolve('test'); + }) + .then(response => { + // Just to make sure we can chain Promises + expect(response).equal('test'); + }); + }); - it('--> is HexColor', () => { - const { error } = validate({ color: '#fff' }); - const { error: error2 } = validate({ color: 'white' }); + it('properties passed ok', () => { + const { error } = validate({ name: 'John', lastname: 'Snow' }); - expect(error).equal(null); - expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - }); + expect(error).equal(null); + }); - it('--> is customFieldWithEmbeddedEntity ok', () => { - const { error } = validate({ - customFieldWithEmbeddedEntity: { - embeddedEntity: { - value: 6, - }, - }, - }); + it('properties passed ko', () => { + const { error } = validate({ unknown: 123 }); - expect(error).equal(null); - }); - - it('--> is customFieldWithEmbeddedEntity ko', () => { - const { error } = validate({ - customFieldWithEmbeddedEntity: { - embeddedEntity: { - value: 2, - }, - }, - }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); - }); + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_NOT_ALLOWED); + }); - it('--> is custom function (array containing objectsj)', () => { - const validateFn = obj => { - if (!Array.isArray(obj)) { - return false; - } - - return obj.every(item => ( - item !== null && typeof item === 'object' - )); - }; - - schema = new Schema({ - arrOfObjs: { - type: 'array', - validate: { - rule: validateFn, - }, - }, - }); - - const error1 = validate({ arrOfObjs: [{ name: 'foo' }, { name: 'bar' }] }).error; - const error2 = validate({ arrOfObjs: 'string' }).error; - const error3 = validate({ arrOfObjs: ['string'] }).error; - const error4 = validate({ arrOfObjs: [{ name: 'foo' }, 'string'] }).error; - - expect(error1).equal(null); - expect(error2.code).equal(gstoreErrors.errorCodes.ERR_VALIDATION); - expect(error3.code).equal(gstoreErrors.errorCodes.ERR_VALIDATION); - expect(error4.code).equal(gstoreErrors.errorCodes.ERR_VALIDATION); - }); - - it('--> only accept value in range of values', () => { - const { error } = validate({ type: 'other' }); - - expect(error.errors[0].code).equal(errorCodes.ERR_PROP_IN_RANGE); - }); + it('should remove virtuals before validating', () => { + const { error } = validate({ fullname: 'John Snow' }); + + expect(error).equal(null); + }); + + it('accept unkwown properties when "explicityOnly" set to false', () => { + schema = new Schema({ name: { type: 'string' } }, { explicitOnly: false }); + + const { error } = validate({ unknown: 123 }); + + expect(error).equal(null); + }); + + it('required property', () => { + schema = new Schema({ + name: { type: 'string' }, + email: { type: 'string', required: true }, + }); + + const { error } = validate({ name: 'John Snow', email: '' }); + const { error: error2 } = validate({ name: 'John Snow', email: ' ' }); + const { error: error3 } = validate({ name: 'John Snow', email: null }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_REQUIRED); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_REQUIRED); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_REQUIRED); + }); + + it("don't validate empty value", () => { + const { error } = validate({ email: undefined }); + const { error: error2 } = validate({ email: null }); + const { error: error3 } = validate({ email: '' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + }); + + it('no type validation', () => { + const { error } = validate({ street: 123 }); + const { error: error2 } = validate({ street: '123' }); + const { error: error3 } = validate({ street: true }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + }); + + it('--> Datstore Key ok', () => { + const company = ds.key(['EntityKind', 123]); + const { error } = validate({ company }); + + expect(error).equal(null); + }); + + it('--> Datstore Key ko', () => { + const { error } = validate({ company: 123 }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error.errors[0].ref).equal('key.base'); + }); + + it('--> Datstore Key ko', () => { + const address = ds.key(['WrongReference', 123]); + const { error } = validate({ address }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error.errors[0].ref).equal('key.entityKind'); + }); + + it('--> string', () => { + const { error } = validate({ name: 123 }); + + expect(error).not.equal(null); + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> number', () => { + const { error } = validate({ age: 'string' }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> int', () => { + const { error } = validate({ age: ds.int('7') }); + const { error: error2 } = validate({ age: ds.int(7) }); + const { error: error3 } = validate({ age: 7 }); + const { error: error4 } = validate({ age: ds.int('string') }); + const { error: error5 } = validate({ age: 'string' }); + const { error: error6 } = validate({ age: '7' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + expect(error4.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> double', () => { + const { error } = validate({ price: ds.double('1.2') }); + const { error: error2 } = validate({ price: ds.double(7.0) }); + const { error: error3 } = validate({ price: 7 }); + const { error: error4 } = validate({ price: 7.59 }); + const { error: error5 } = validate({ price: ds.double('str') }); + const { error: error6 } = validate({ price: 'string' }); + const { error: error7 } = validate({ price: '7' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + expect(error4).equal(null); + expect(error5.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error7.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> buffer', () => { + const { error } = validate({ icon: Buffer.from('\uD83C\uDF69') }); + const { error: error2 } = validate({ icon: 'string' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> boolean', () => { + const { error } = validate({ modified: true }); + const { error: error2 } = validate({ modified: 'string' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> object', () => { + const { error } = validate({ prefs: { check: true } }); + const { error: error2 } = validate({ prefs: 'string' }); + const { error: error3 } = validate({ prefs: [123] }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> geoPoint', () => { + // datastore geoPoint + const { error } = validate({ + location: ds.geoPoint({ + latitude: 40.6894, + longitude: -74.0447, + }), + }); + + // valid geo object + const { error: error2 } = validate({ + location: { + latitude: 40.68942342541, + longitude: -74.044743654572, + }, + }); + + const { error: error3 } = validate({ location: 'string' }); + const { error: error4 } = validate({ location: true }); + const { error: error5 } = validate({ location: { longitude: 999, latitude: 'abc' } }); + const { error: error6 } = validate({ location: { longitude: 40.6895 } }); + const { error: error7 } = validate({ location: { longitude: '120.123', latitude: '40.12345678' } }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error4.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error7.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> array ok', () => { + const { error } = validate({ tags: [] }); + + expect(error).equal(null); + }); + + it('--> array ko', () => { + const { error } = validate({ tags: {} }); + const { error: error2 } = validate({ tags: 'string' }); + const { error: error3 } = validate({ tags: 123 }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> date ok', () => { + const { error } = validate({ birthday: '2015-01-01' }); + const { error: error2 } = validate({ birthday: new Date() }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> date ko', () => { + const { error } = validate({ birthday: '01-2015-01' }); + const { error: error2 } = validate({ birthday: '01-01-2015' }); + const { error: error3 } = validate({ birthday: '2015/01/01' }); + const { error: error4 } = validate({ birthday: '01/01/2015' }); + const { error: error5 } = validate({ birthday: 12345 }); // No number allowed + const { error: error6 } = validate({ birthday: 'string' }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error4.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(ERROR_CODES.ERR_PROP_TYPE); + }); + + it('--> isURL ok', () => { + const { error } = validate({ website: 'http://google.com' }); + const { error: error2 } = validate({ website: 'google.com' }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> isURL ko', () => { + const { error } = validate({ website: 'domain.k' }); + const { error: error2 } = validate({ website: 123 }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + }); + + it('--> isEmail ok', () => { + const { error } = validate({ email: 'john@snow.com' }); + + expect(error).equal(null); + }); + + it('--> isEmail ko', () => { + const { error } = validate({ email: 'john@snow' }); + const { error: error2 } = validate({ email: 'john@snow.' }); + const { error: error3 } = validate({ email: 'john@snow.k' }); + const { error: error4 } = validate({ email: 'johnsnow.com' }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + expect(error3.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + expect(error4.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + }); + + it('--> is IP ok', () => { + const { error } = validate({ ip: '127.0.0.1' }); + const { error: error2 } = validate({ ip2: '127.0.0.1' }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> is IP ko', () => { + const { error } = validate({ ip: 'fe80::1c2e:f014:10d8:50f5' }); + const { error: error2 } = validate({ ip: '1.1.1' }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + }); + + it('--> is HexColor', () => { + const { error } = validate({ color: '#fff' }); + const { error: error2 } = validate({ color: 'white' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + }); + + it('--> is customFieldWithEmbeddedEntity ok', () => { + const { error } = validate({ + customFieldWithEmbeddedEntity: { + embeddedEntity: { + value: 6, + }, + }, + }); + + expect(error).equal(null); + }); + + it('--> is customFieldWithEmbeddedEntity ko', () => { + const { error } = validate({ + customFieldWithEmbeddedEntity: { + embeddedEntity: { + value: 2, + }, + }, + }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_VALUE); + }); + + it('--> is custom function (array containing objectsj)', () => { + const validateFn = obj => { + if (!Array.isArray(obj)) { + return false; + } + + return obj.every(item => item !== null && typeof item === 'object'); + }; + + schema = new Schema({ + arrOfObjs: { + type: 'array', + validate: { + rule: validateFn, + }, + }, + }); + + const error1 = validate({ arrOfObjs: [{ name: 'foo' }, { name: 'bar' }] }).error; + const error2 = validate({ arrOfObjs: 'string' }).error; + const error3 = validate({ arrOfObjs: ['string'] }).error; + const error4 = validate({ arrOfObjs: [{ name: 'foo' }, 'string'] }).error; + + expect(error1).equal(null); + expect(error2.code).equal(ERROR_CODES.ERR_VALIDATION); + expect(error3.code).equal(ERROR_CODES.ERR_VALIDATION); + expect(error4.code).equal(ERROR_CODES.ERR_VALIDATION); + }); + + it('--> only accept value in range of values', () => { + const { error } = validate({ type: 'other' }); + + expect(error.errors[0].code).equal(ERROR_CODES.ERR_PROP_IN_RANGE); + }); }); describe('Joi Validation', () => { - let schema; - - const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind'); - - beforeEach(() => { - schema = new Schema({ - name: { joi: Joi.string().required() }, - color: { joi: Joi.valid('a', 'b') }, - birthyear: { joi: Joi.number().integer().min(1900).max(2013) }, - email: { joi: Joi.string().email() }, - }, { - joi: { options: { stripUnknown: false } }, - }); - }); - - it('should validate with Joi', () => { - const { error } = validate({ name: 123 }); - const { error: error2 } = validate({ name: 'John', color: 'c' }); - const { error: error3 } = validate({ name: 'John', birthyear: 1877 }); - const { error: error4 } = validate({ name: 'John', email: 'abc' }); - const { error: error5 } = validate({ name: 'John', unknownProp: 'abc' }); - - expect(error).not.equal(null); - expect(error2.details[0].type).equal('any.allowOnly'); - expect(error3.details[0].type).equal('number.min'); - expect(error4.details[0].type).equal('string.email'); - expect(error5.details[0].type).equal('object.allowUnknown'); - }); - - it('should accept extra validation on top of the schema', () => { - schema = new Schema({ - name: { joi: Joi.string() }, - lastname: { joi: Joi.string() }, - }, { - joi: { - extra: { - with: ['name', 'lastname'], - unknownMethod: 'shouldBeIgnored', - }, - }, - }); - - const { error } = validate({ name: 'John' }); - - expect(error.details[0].type).equal('object.with'); - }); + let schema; + + const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind'); + + beforeEach(() => { + schema = new Schema( + { + name: { joi: Joi.string().required() }, + color: { joi: Joi.valid('a', 'b') }, + birthyear: { + joi: Joi.number() + .integer() + .min(1900) + .max(2013), + }, + email: { joi: Joi.string().email() }, + }, + { + joi: { options: { stripUnknown: false } }, + }, + ); + }); + + it('should validate with Joi', () => { + const { error } = validate({ name: 123 }); + const { error: error2 } = validate({ name: 'John', color: 'c' }); + const { error: error3 } = validate({ name: 'John', birthyear: 1877 }); + const { error: error4 } = validate({ name: 'John', email: 'abc' }); + const { error: error5 } = validate({ name: 'John', unknownProp: 'abc' }); + + expect(error).not.equal(null); + expect(error2.details[0].type).equal('any.allowOnly'); + expect(error3.details[0].type).equal('number.min'); + expect(error4.details[0].type).equal('string.email'); + expect(error5.details[0].type).equal('object.allowUnknown'); + }); + + it('should accept extra validation on top of the schema', () => { + schema = new Schema( + { + name: { joi: Joi.string() }, + lastname: { joi: Joi.string() }, + }, + { + joi: { + extra: { + with: ['name', 'lastname'], + unknownMethod: 'shouldBeIgnored', + }, + }, + }, + ); - it('should accept an "option" object', () => { - schema = new Schema({ - name: { joi: Joi.string().required() }, - }, { - joi: { - options: { - allowUnknown: true, - }, - }, - }); + const { error } = validate({ name: 'John' }); + + expect(error.details[0].type).equal('object.with'); + }); + + it('should accept an "option" object', () => { + schema = new Schema( + { + name: { joi: Joi.string().required() }, + }, + { + joi: { + options: { + allowUnknown: true, + }, + }, + }, + ); - const { error } = validate({ name: 'John', unknownProp: 'abc' }); + const { error } = validate({ name: 'John', unknownProp: 'abc' }); - expect(error).equal(null); - }); + expect(error).equal(null); + }); - it('should set "stripUnknown" according to "explicitOnly" setting', () => { - schema = new Schema({ name: { joi: Joi.string() } }, { explicitOnly: false }); - const schema2 = new Schema({ name: { joi: Joi.string() } }); + it('should set "stripUnknown" according to "explicitOnly" setting', () => { + schema = new Schema({ name: { joi: Joi.string() } }, { explicitOnly: false }); + const schema2 = new Schema({ name: { joi: Joi.string() } }); - const { error, value } = validate({ name: 'John', unknownProp: 'abc' }); - const { error: error2, value: value2 } = validation.validate({ name: 'John', unknownProp: 'abc' }, schema2, ''); + const { error, value } = validate({ name: 'John', unknownProp: 'abc' }); + const { error: error2, value: value2 } = validation.validate({ name: 'John', unknownProp: 'abc' }, schema2, ''); - expect(error).equal(null); - expect(value.unknownProp).equal('abc'); - expect(error2).not.equal(null); - expect(value2.unknownProp).equal('abc'); - }); + expect(error).equal(null); + expect(value.unknownProp).equal('abc'); + expect(error2).not.equal(null); + expect(value2.unknownProp).equal('abc'); + }); }); diff --git a/test/index-test.js b/test/index-test.js index bf6a202..cae60fb 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,4 +1,3 @@ - 'use strict'; /** @@ -13,8 +12,8 @@ const { Datastore } = require('@google-cloud/datastore'); const { expect, assert } = chai; const ds = new Datastore({ - namespace: 'com.mydomain', - apiEndpoint: 'http://localhost:8080', + namespace: 'com.mydomain', + apiEndpoint: 'http://localhost:8080', }); const { Gstore, instances } = require('../lib'); @@ -25,275 +24,247 @@ const pkg = require('../package.json'); const Transaction = require('./mocks/transaction'); describe('gstore-node', () => { - let schema; - let ModelInstance; - let transaction; + let schema; + let ModelInstance; + let transaction; - beforeEach(() => { - gstore.models = {}; - gstore.modelSchemas = {}; - - schema = new Schema({ - name: { type: 'string' }, - email: { type: 'string', read: false }, - }); - ModelInstance = gstore.model('Blog', schema, {}); - - transaction = new Transaction(); - sinon.spy(transaction, 'save'); - sinon.spy(transaction, 'commit'); - sinon.spy(transaction, 'rollback'); - }); + beforeEach(() => { + gstore.models = {}; + gstore.modelSchemas = {}; - afterEach(() => { - transaction.save.restore(); - transaction.commit.restore(); - transaction.rollback.restore(); + schema = new Schema({ + name: { type: 'string' }, + email: { type: 'string', read: false }, }); + ModelInstance = gstore.model('Blog', schema, {}); + + transaction = new Transaction(); + sinon.spy(transaction, 'save'); + sinon.spy(transaction, 'commit'); + sinon.spy(transaction, 'rollback'); + }); + + afterEach(() => { + transaction.save.restore(); + transaction.commit.restore(); + transaction.rollback.restore(); + }); + + it('should initialized its properties', () => { + assert.isDefined(gstore.models); + assert.isDefined(gstore.modelSchemas); + assert.isDefined(gstore.Schema); + }); + + it('should save ds instance', () => { + gstore.connect(ds); + expect(gstore.ds).to.equal(ds); + expect(gstore.ds.constructor.name).equal('Datastore'); + }); + + it('should throw an error if ds passed on connect is not a Datastore instance', () => { + const fn = () => { + gstore.connect({}); + }; + + expect(fn).to.throw(); + }); + + describe('should create models', () => { + beforeEach(() => { + schema = new gstore.Schema({ + title: { type: 'string' }, + }); - it('should initialized its properties', () => { - assert.isDefined(gstore.models); - assert.isDefined(gstore.modelSchemas); - assert.isDefined(gstore.options); - assert.isDefined(gstore.Schema); + gstore.models = {}; + gstore.modelSchemas = {}; + gstore.options = {}; }); - it('should save ds instance', () => { - gstore.connect(ds); - expect(gstore.ds).to.equal(ds); - expect(gstore.ds.constructor.name).equal('Datastore'); + it('and add it with its schema to the cache', () => { + const Model = gstore.model('Blog', schema); + + assert.isDefined(Model); + assert.isDefined(gstore.models.Blog); }); - it('should throw an error if ds passed on connect is not a Datastore instance', () => { - const fn = () => { - gstore.connect({}); - }; + it('and attach schema to compiled Model', () => { + const Blog = gstore.model('Blog', schema); + const schemaUser = new gstore.Schema({ name: { type: 'string' } }); + const User = gstore.model('User', schemaUser); - expect(fn).to.throw(); + expect(Blog.schema).not.equal(User.schema); }); - describe('should create models', () => { - beforeEach(() => { - schema = new gstore.Schema({ - title: { type: 'string' }, - }); + it('reading them from cache', () => { + const mockModel = { schema }; + gstore.models.Blog = mockModel; - gstore.models = {}; - gstore.modelSchemas = {}; - gstore.options = {}; - }); + const model = gstore.model('Blog'); - it('and add it with its schema to the cache', () => { - const Model = gstore.model('Blog', schema); + expect(model).equal(mockModel); + }); - assert.isDefined(Model); - assert.isDefined(gstore.models.Blog); - assert.isDefined(gstore.modelSchemas.Blog); - }); + it('and throw error if trying to override schema', () => { + const newSchema = new gstore.Schema({}); + const mockModel = { schema }; + gstore.models.Blog = mockModel; - it('and convert schema object to Schema class instance', () => { - schema = {}; + const fn = () => gstore.model('Blog', newSchema); - const Model = gstore.model('Blog', schema); + expect(fn).to.throw(Error); + }); - expect(Model.schema.constructor.name).to.equal('Schema'); - }); + it('and throw error if no Schema is passed', () => { + const fn = () => gstore.model('Blog'); - it('and attach schema to compiled Model', () => { - const Blog = gstore.model('Blog', schema); - const schemaUser = new gstore.Schema({ name: { type: 'string' } }); - const User = gstore.model('User', schemaUser); + expect(fn).to.throw(Error); + }); + }); - expect(Blog.schema).not.equal(User.schema); - }); + it('should return the models names', () => { + gstore.models = { Blog: {}, Image: {} }; - it('and not add them to cache if told so', () => { - const options = { cache: false }; + const names = gstore.modelNames(); - gstore.model('Image', schema, options); + expect(names).eql(['Blog', 'Image']); + }); - assert.isUndefined(gstore.models.Image); - }); + it('should return the package version', () => { + const { version } = pkg; - it('reading them from cache', () => { - const mockModel = { schema }; - gstore.models.Blog = mockModel; + expect(gstore.version).equal(version); + }); - const model = gstore.model('Blog', schema); + it('should create shortcut of datastore.transaction', () => { + sinon.spy(ds, 'transaction'); - expect(model).equal(mockModel); - }); + const trans = gstore.transaction(); - it('allowing to pass an existing Schema', () => { - gstore.modelSchemas.Blog = schema; + expect(ds.transaction.called).equal(true); + expect(trans.constructor.name).equal('Transaction'); + }); - const model = gstore.model('Blog', schema); + describe('save() alias', () => { + beforeEach(() => { + gstore.connect(ds); + sinon.stub(ds, 'save').resolves(); + }); - expect(model.schema).to.equal(schema); - }); + afterEach(() => { + ds.save.restore(); + }); - it('and throw error if trying to override schema', () => { - const newSchema = new gstore.Schema({}); - const mockModel = { schema }; - gstore.models.Blog = mockModel; + it('should convert entity instances to datastore Format', () => { + const entity1 = new ModelInstance({ name: 'John' }); + const entity2 = new ModelInstance({ name: 'Mick' }); - const fn = () => gstore.model('Blog', newSchema); + return gstore.save([entity1, entity2]).then(() => { + const { args } = ds.save.getCall(0); + const firstEntity = args[0][0]; + assert.isUndefined(firstEntity.__className); + expect(Object.keys(firstEntity)).deep.equal(['key', 'data', 'excludeLargeProperties']); + }); + }); - expect(fn).to.throw(Error); - }); + it('should work inside a transaction', () => { + const entity = new ModelInstance({ name: 'John' }); - it('and throw error if no Schema is passed', () => { - const fn = () => gstore.model('Blog'); + gstore.save(entity, transaction); - expect(fn).to.throw(Error); - }); + expect(transaction.save.called).equal(true); + expect(ds.save.called).equal(false); }); - it('should return the models names', () => { - gstore.models = { Blog: {}, Image: {} }; - - const names = gstore.modelNames(); + it('should throw an error if no entities passed', () => { + const func = () => gstore.save(); - expect(names).eql(['Blog', 'Image']); + expect(func).to.throw('No entities passed'); }); - it('should return the package version', () => { - const { version } = pkg; - - expect(gstore.version).equal(version); + it('should validate entity before saving', done => { + schema = new Schema({ name: { type: String } }); + const Model = gstore.model('TestValidate', schema); + const entity1 = new Model({ name: 'abc' }); + const entity2 = new Model({ name: 123 }); + const entity3 = new Model({ name: 'def' }); + sinon.spy(entity1, 'validate'); + sinon.spy(entity3, 'validate'); + + gstore.save([entity1, entity2, entity3], undefined, { validate: true }).catch(e => { + expect(e.code).equal('ERR_VALIDATION'); + expect(entity1.validate.called).equal(true); + expect(entity3.validate.called).equal(false); // fail fast, exit validation + done(); + }); }); - it('should create shortcut of datastore.transaction', () => { - sinon.spy(ds, 'transaction'); + it('should allow to pass a save method ("insert", "update", "upsert")', () => { + const entity = new ModelInstance({ name: 'John' }); - const trans = gstore.transaction(); + return gstore.save(entity, undefined, { method: 'insert' }).then(() => { + const { args } = ds.save.getCall(0); + expect(args[0].method).equal('insert'); + }); + }); + }); - expect(ds.transaction.called).equal(true); - expect(trans.constructor.name).equal('Transaction'); + describe('cache', () => { + /* eslint-disable global-require */ + it('should not set any cache by default', () => { + const gstoreNoCache = new Gstore(); + assert.isUndefined(gstoreNoCache.cache); }); - describe('save() alias', () => { - beforeEach(() => { - gstore.connect(ds); - sinon.stub(ds, 'save').resolves(); - }); - - afterEach(() => { - ds.save.restore(); - }); - - it('should convert entity instances to datastore Format', () => { - const entity1 = new ModelInstance({ name: 'John' }); - const entity2 = new ModelInstance({ name: 'Mick' }); - - return gstore.save([entity1, entity2]).then(() => { - const { args } = ds.save.getCall(0); - const firstEntity = args[0][0]; - assert.isUndefined(firstEntity.className); - expect(Object.keys(firstEntity)).deep.equal(['key', 'data', 'excludeLargeProperties']); - }); - }); - - it('should work inside a transaction', () => { - const entity = new ModelInstance({ name: 'John' }); - - gstore.save(entity, transaction); - - expect(transaction.save.called).equal(true); - expect(ds.save.called).equal(false); - }); - - it('should throw an error if no entities passed', () => { - const func = () => gstore.save(); - - expect(func).to.throw('No entities passed'); - }); - - it('should validate entity before saving', done => { - schema = new Schema({ name: { type: String } }); - const Model = gstore.model('TestValidate', schema); - const entity1 = new Model({ name: 'abc' }); - const entity2 = new Model({ name: 123 }); - const entity3 = new Model({ name: 'def' }); - sinon.spy(entity1, 'validate'); - sinon.spy(entity3, 'validate'); - - gstore.save([entity1, entity2, entity3], undefined, { validate: true }) - .catch(e => { - expect(e.code).equal('ERR_VALIDATION'); - expect(entity1.validate.called).equal(true); - expect(entity3.validate.called).equal(false); // fail fast, exit validation - done(); - }); - }); - - it('should allow to pass a save method ("insert", "update", "upsert")', () => { - const entity = new ModelInstance({ name: 'John' }); - - return gstore.save(entity, undefined, { method: 'insert' }) - .then(() => { - const { args } = ds.save.getCall(0); - expect(args[0].method).equal('insert'); - }); - }); + it('should set the default cache to memory lru-cache', () => { + const gstoreWithCache = new Gstore({ cache: true }); + gstoreWithCache.connect(ds); + + const { cache } = gstoreWithCache; + assert.isDefined(cache); + expect(cache.stores.length).equal(1); + expect(cache.stores[0].store).equal('memory'); }); - describe('cache', () => { - /* eslint-disable global-require */ - it('should not set any cache by default', () => { - const gstoreNoCache = new Gstore(); - assert.isUndefined(gstoreNoCache.cache); - }); - - it('should set the default cache to memory lru-cache', () => { - const gstoreWithCache = new Gstore({ cache: true }); - gstoreWithCache.connect(ds); - - const { cache } = gstoreWithCache; - assert.isDefined(cache); - expect(cache.stores.length).equal(1); - expect(cache.stores[0].store).equal('memory'); - }); - - it('should create cache instance from config passed', () => { - const cacheSettings = { - stores: [{ store: 'memory' }], - config: { - ttl: { - keys: 12345, - queries: 6789, - }, - }, - }; - const gstoreWithCache = new Gstore({ cache: cacheSettings }); - gstoreWithCache.connect(ds); - const { cache } = gstoreWithCache; - - expect(gstoreWithCache.cache).equal(cache); - expect(gstoreWithCache.cache.config.ttl.keys).equal(12345); - }); + it('should create cache instance from config passed', () => { + const cacheSettings = { + stores: [{ store: 'memory' }], + config: { + ttl: { + keys: 12345, + queries: 6789, + }, + }, + }; + const gstoreWithCache = new Gstore({ cache: cacheSettings }); + gstoreWithCache.connect(ds); + const { cache } = gstoreWithCache; + + expect(gstoreWithCache.cache).equal(cache); + expect(gstoreWithCache.cache.config.ttl.keys).equal(12345); }); + }); - describe('multi instances', () => { - it('should cache instances', () => { - const gstore1 = new Gstore(); - const gstore2 = new Gstore({ cache: true }); + describe('multi instances', () => { + it('should cache instances', () => { + const gstore1 = new Gstore(); + const gstore2 = new Gstore({ cache: true }); - instances.set('instance-1', gstore1); - instances.set('instance-2', gstore2); + instances.set('instance-1', gstore1); + instances.set('instance-2', gstore2); - const cached1 = instances.get('instance-1'); - const cached2 = instances.get('instance-2'); - expect(cached1).equal(gstore1); - expect(cached2).equal(gstore2); - }); + const cached1 = instances.get('instance-1'); + const cached2 = instances.get('instance-2'); + expect(cached1).equal(gstore1); + expect(cached2).equal(gstore2); + }); - it('should throw Error if wrong config', () => { - const func1 = () => new Gstore(0); - const func2 = () => new Gstore('some-string'); + it('should throw Error if wrong config', () => { + const func1 = () => new Gstore(0); + const func2 = () => new Gstore('some-string'); - expect(func1).throw(); - expect(func2).throw(); - }); + expect(func1).throw(); + expect(func2).throw(); }); + }); }); diff --git a/test/integration/cache.js b/test/integration/cache.js index 2002e69..6ba7f5c 100644 --- a/test/integration/cache.js +++ b/test/integration/cache.js @@ -1,12 +1,9 @@ -/* eslint-disable no-unused-expressions */ - 'use strict'; const redisStore = require('cache-manager-redis-store'); const chai = require('chai'); const Chance = require('chance'); const { Datastore } = require('@google-cloud/datastore'); -const { argv } = require('yargs'); const { Gstore } = require('../../lib'); @@ -17,72 +14,69 @@ const { expect } = chai; const allKeys = []; const cleanUp = cb => { - ds.delete(allKeys).then(cb); + ds.delete(allKeys).then(cb); }; const addKey = key => { - if (key) { - allKeys.push(key); - } + if (key) { + allKeys.push(key); + } }; describe('Integration Tests (Cache)', () => { - let gstore; - let schema; - let Schema; - let Model; - - beforeEach(function integrationTest() { - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } - if (!gstore) { - gstore = new Gstore({ - cache: { - stores: [{ - store: redisStore, - }], - config: { - ttl: { - keys: 600, - queries: 600, - }, - }, - }, - }); - gstore.connect(ds); - } + let gstore; + let schema; + let Schema; + let Model; - ({ Schema } = gstore); + beforeEach(() => { + if (!gstore) { + gstore = new Gstore({ + cache: { + stores: [ + { + store: redisStore, + }, + ], + config: { + ttl: { + keys: 600, + queries: 600, + }, + }, + }, + }); + gstore.connect(ds); + } - gstore.models = {}; - gstore.modelSchemas = {}; + ({ Schema } = gstore); - schema = new Schema({ - email: { - type: String, - validate: 'isEmail', - required: true, - }, - }); + gstore.models = {}; + gstore.modelSchemas = {}; - Model = gstore.model('CacheTests-User', schema); + schema = new Schema({ + email: { + type: String, + validate: 'isEmail', + required: true, + }, }); - afterEach(done => { - cleanUp(() => done()); - }); + Model = gstore.model('CacheTests-User', schema); + }); + + afterEach(done => { + cleanUp(() => done()); + }); - it('should set KEY symbol on query result', () => { - const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz0123456789' }); - const user = new Model({ email: 'test@test.com' }, id); - return user.save().then(entity => { - addKey(entity.entityKey); - return Model.get(entity.entityKey.name) - .then(e => { - expect(e.email).equal('test@test.com'); - }); - }); + it('should set KEY symbol on query result', () => { + const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz0123456789' }); + const user = new Model({ email: 'test@test.com' }, id); + return user.save().then(entity => { + addKey(entity.entityKey); + return Model.get(entity.entityKey.name).then(e => { + expect(e.email).equal('test@test.com'); + }); }); + }); }); diff --git a/test/integration/entity.js b/test/integration/entity.js index 681afc3..30a2456 100644 --- a/test/integration/entity.js +++ b/test/integration/entity.js @@ -3,7 +3,6 @@ const chai = require('chai'); const Chance = require('chance'); const { Datastore } = require('@google-cloud/datastore'); -const { argv } = require('yargs'); const { Gstore } = require('../../lib'); const ds = new Datastore({ projectId: 'gstore-integration-tests' }); @@ -15,9 +14,9 @@ const { expect, assert } = chai; const userSchema = new Schema({ address: { type: Schema.Types.Key } }); const addressBookSchema = new Schema({ label: { type: String } }); const addressSchema = new Schema({ - city: { type: String }, - country: { type: String }, - addressBook: { type: Schema.Types.Key }, + city: { type: String }, + country: { type: String }, + addressBook: { type: Schema.Types.Key }, }); const chance = new Chance(); @@ -29,126 +28,110 @@ const AddressModel = gstore.model('EntityTests-Address', addressSchema); const AddressBookModel = gstore.model('EntityTests-AddressBook', addressBookSchema); const getId = () => { - const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); - if (generatedIds.indexOf(id) >= 0) { - return getId(); - } - generatedIds.push(id); - return id; + const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); + if (generatedIds.indexOf(id) >= 0) { + return getId(); + } + generatedIds.push(id); + return id; }; const getAddressBook = () => { - const key = AddressBookModel.key(getId()); - allKeys.push(key); - const data = { label: chance.string() }; - const addressBook = new AddressBookModel(data, null, null, null, key); - return addressBook; + const key = AddressBookModel.key(getId()); + allKeys.push(key); + const data = { label: chance.string() }; + const addressBook = new AddressBookModel(data, null, null, null, key); + return addressBook; }; const getAddress = (addressBookEntity = null) => { - const key = AddressModel.key(getId()); - allKeys.push(key); - const data = { city: chance.city(), country: chance.country(), addressBook: addressBookEntity.entityKey }; - const address = new AddressModel(data, null, null, null, key); - return address; + const key = AddressModel.key(getId()); + allKeys.push(key); + const data = { city: chance.city(), country: chance.country(), addressBook: addressBookEntity.entityKey }; + const address = new AddressModel(data, null, null, null, key); + return address; }; const getUser = (addressEntity, id = getId()) => { - const key = UserModel.key(id); - allKeys.push(key); - const data = { address: addressEntity.entityKey }; - const user = new UserModel(data, null, null, null, key); - return user; + const key = UserModel.key(id); + allKeys.push(key); + const data = { address: addressEntity.entityKey }; + const user = new UserModel(data, null, null, null, key); + return user; }; -const cleanUp = () => ds.delete(allKeys).then(() => Promise.all([ - UserModel.deleteAll(), - AddressModel.deleteAll(), - AddressBookModel.deleteAll(), -])) +const cleanUp = () => + ds + .delete(allKeys) + .then(() => Promise.all([UserModel.deleteAll(), AddressModel.deleteAll(), AddressBookModel.deleteAll()])) .catch(err => { console.log('Error cleaning up'); // eslint-disable-line console.log(err); // eslint-disable-line }); describe('Entity (Integration Tests)', () => { - const addressBook = getAddressBook(); - const address = getAddress(addressBook); - let user; - - before(function integrationTest() { - if (argv.int !== true) { - this.skip(); - } - generatedIds = []; - return gstore.save([addressBook, address]); - }); - - after(function afterAllIntTest() { - if (argv.int !== true) { - this.skip(); - } - return cleanUp(); - }); - - beforeEach(function integrationTest() { - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } - user = getUser(address); - }); - - describe('save()', () => { - it('should replace a populated ref to its key before saving', () => ( - user.populate() - .then(() => user.save()) - .then(() => UserModel.get(user.entityKey.name)) - .then(entityFetched => { - expect(entityFetched.entityData.address).deep.equal(address.entityKey); - }) - )); - - it('should add the id or name to the entity', async () => { - const entity1 = await user.save(); - expect(entity1.id).equal(entity1.entityKey.name); - - const user2 = getUser(address, 1234); - const entity2 = await user2.save(); - - expect(entity2.id).equal(entity2.entityKey.id); - }); - }); - - describe('populate()', () => { - it('should populate the user address', () => ( - user.populate() - .populate('unknown') // allow chaining populate() calls - .then(() => { - expect(user.address.city).equal(address.city); - expect(user.address.country).equal(address.country); - expect(user.entityData.unknown).equal(null); - }) - )); - - it('should only populate the user address country', () => ( - user.populate('address', 'country') - .then(() => { - expect(user.address.country).equal(address.country); - assert.isUndefined(user.address.city); - }) - )); - - it('should allow deep fetching', () => ( - user - .populate() - .populate('address.addressBook', ['label', 'unknown']) - .then(() => { - expect(user.address.city).equal(address.city); - expect(user.address.country).equal(address.country); - expect(user.address.addressBook.label).equal(addressBook.label); - expect(user.address.addressBook.unknown).equal(null); - }) - )); + const addressBook = getAddressBook(); + const address = getAddress(addressBook); + let user; + + before(() => { + generatedIds = []; + return gstore.save([addressBook, address]); + }); + + after(() => cleanUp()); + + beforeEach(() => { + user = getUser(address); + }); + + describe('save()', () => { + it('should replace a populated ref to its key before saving', () => + user + .populate() + .then(() => user.save()) + .then(() => UserModel.get(user.entityKey.name)) + .then(entityFetched => { + expect(entityFetched.entityData.address).deep.equal(address.entityKey); + })); + + it('should add the id or name to the entity', async () => { + const entity1 = await user.save(); + expect(entity1.id).equal(entity1.entityKey.name); + + const user2 = getUser(address, 1234); + const entity2 = await user2.save(); + + expect(entity2.id).equal(entity2.entityKey.id); }); + }); + + describe('populate()', () => { + it('should populate the user address', () => + user + .populate() + .populate('unknown') // allow chaining populate() calls + .then(() => { + expect(user.address.city).equal(address.city); + expect(user.address.country).equal(address.country); + expect(user.entityData.unknown).equal(null); + })); + + it('should only populate the user address country', () => + user.populate('address', 'country').then(() => { + expect(user.address.country).equal(address.country); + assert.isUndefined(user.address.city); + })); + + it('should allow deep fetching', () => + user + .populate() + .populate('address.addressBook', ['label', 'unknown']) + .then(() => { + expect(user.address.city).equal(address.city); + expect(user.address.country).equal(address.country); + expect(user.address.addressBook.label).equal(addressBook.label); + expect(user.address.addressBook.unknown).equal(null); + })); + }); }); diff --git a/test/integration/index.js b/test/integration/index.js index 2dfa48d..243aec1 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -3,7 +3,6 @@ const chai = require('chai'); const Chance = require('chance'); const { Datastore } = require('@google-cloud/datastore'); -const { argv } = require('yargs'); const { Gstore } = require('../../lib'); const gstore = new Gstore(); @@ -21,60 +20,46 @@ const allKeys = []; const UserModel = gstore.model('GstoreTests-User', userSchema); const getId = () => { - const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); - if (generatedIds.indexOf(id) >= 0) { - return getId(); - } - generatedIds.push(id); - return id; + const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); + if (generatedIds.indexOf(id) >= 0) { + return getId(); + } + generatedIds.push(id); + return id; }; const getUser = () => { - const key = UserModel.key(getId()); - allKeys.push(key); - const data = { name: chance.string() }; - const user = new UserModel(data, null, null, null, key); - return user; + const key = UserModel.key(getId()); + allKeys.push(key); + const data = { name: chance.string() }; + const user = new UserModel(data, null, null, null, key); + return user; }; -const cleanUp = () => ds.delete(allKeys).then(() => UserModel.deleteAll()) +const cleanUp = () => + ds + .delete(allKeys) + .then(() => UserModel.deleteAll()) .catch(err => { console.log('Error cleaning up'); // eslint-disable-line console.log(err); // eslint-disable-line }); describe('Gstore (Integration Tests)', () => { - before(function integrationTest() { - if (argv.int !== true) { - this.skip(); - } - generatedIds = []; - }); - - after(function afterAllIntTest() { - if (argv.int !== true) { - this.skip(); - } - return cleanUp(); - }); + before(() => { + generatedIds = []; + }); - beforeEach(function integrationTest() { - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } - }); + after(() => cleanUp()); - describe('save()', () => { - it('should convert entities to Datastore format and save them', () => { - const users = [getUser(), getUser()]; - return gstore.save(users) - .then(() => ( - UserModel.list() - .then(({ entities: { 0: entity } }) => { - expect([users[0].name, users[1].name]).include(entity.name); - }) - )); - }); + describe('save()', () => { + it('should convert entities to Datastore format and save them', () => { + const users = [getUser(), getUser()]; + return gstore.save(users).then(() => + UserModel.list().then(({ entities: { 0: entity } }) => { + expect([users[0].name, users[1].name]).include(entity.name); + }), + ); }); + }); }); diff --git a/test/integration/model.js b/test/integration/model.js index 6a9e4e9..b094d92 100644 --- a/test/integration/model.js +++ b/test/integration/model.js @@ -7,7 +7,6 @@ const sinon = require('sinon'); const Chance = require('chance'); const Joi = require('@hapi/joi'); const { Datastore } = require('@google-cloud/datastore'); -const { argv } = require('yargs'); const { Gstore } = require('../../lib'); const Entity = require('../../lib/entity'); @@ -16,8 +15,8 @@ const gstore = new Gstore(); const chance = new Chance(); const ds = new Datastore({ - projectId: 'gstore-integration-tests', - keyFilename: '/Users/sebastien/secure-keys/gstore-integration-tests-67ddd52037cf.json', + projectId: 'gstore-integration-tests', + keyFilename: '/Users/sebastien/secure-keys/gstore-integration-tests-67ddd52037cf.json', }); gstore.connect(ds); @@ -30,318 +29,341 @@ const allKeys = []; * We save all saved key so we can delete them after our tests have ran */ const addKey = key => { - allKeys.push(key); + allKeys.push(key); }; const cleanUp = cb => { - ds.delete(allKeys) - .then(cb) - .catch(err => { + ds.delete(allKeys) + .then(cb) + .catch(err => { console.log('Error cleaning up'); // eslint-disable-line console.log(err); // eslint-disable-line - cb(); - }); + cb(); + }); }; const randomName = () => chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz0123456789' }); describe('Model (Integration Tests)', () => { - after(function afterAllIntTest(done) { - if (argv.int !== true) { - this.skip(); - } - cleanUp(() => done()); + after(done => { + cleanUp(() => done()); + }); + + describe('get()', () => { + const { Key } = Schema.Types; + const companySchema = new Schema({ name: { type: String } }); + const userSchema = new Schema({ + name: { type: String }, + email: { type: String }, + company: { type: Key }, + private: { read: false }, }); - - beforeEach(function integrationTest() { - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } + const publicationSchema = new Schema({ title: { type: String }, user: { type: Key } }); + const postSchema = new Schema({ + title: { type: String }, + user: { type: Key }, + publication: { type: Key }, }); - describe('get()', () => { - const { Key } = Schema.Types; - const companySchema = new Schema({ name: { type: String } }); - const userSchema = new Schema({ - name: { type: String }, - email: { type: String }, - company: { type: Key }, - private: { read: false }, - }); - const publicationSchema = new Schema({ title: { type: String }, user: { type: Key } }); - const postSchema = new Schema({ - title: { type: String }, - user: { type: Key }, - publication: { type: Key }, - }); - - const UserModel = gstore.model('ModelTests-User', userSchema); - const CompanyModel = gstore.model('ModelTests-Company', companySchema); - const PostModel = gstore.model('ModelTests-Post', postSchema); - const PublicationModel = gstore.model('ModelTests-Publication', publicationSchema); - - const addCompany = () => { - const name = randomName(); - const company = new CompanyModel({ name }); - return company.save() - .then(({ entityKey }) => { - addKey(entityKey); - return { name, entityKey }; - }); - }; - - const addUser = (company = null) => { - const name = randomName(); - const user = new UserModel({ - name, company, email: chance.email(), private: randomName(), - }, randomName()); - return user.save() - .then(({ entityKey }) => { - addKey(entityKey); - return { name, entityKey }; - }); - }; - - const addPost = (userKey = null, publicationKey = null) => { - const title = randomName(); - const post = new PostModel({ title, user: userKey, publication: publicationKey }, randomName()); - return post.save() - .then(({ entityKey }) => { - addKey(entityKey); - return { title, entityKey }; - }); - }; - - const addPublication = (userKey = null) => { - const title = randomName(); - const publication = new PublicationModel({ title, user: userKey }); - return publication.save() - .then(({ entityKey }) => { - addKey(entityKey); - return { title, entityKey }; - }); - }; - - describe('populate()', () => { - it('should fetch the "user" embedded entities', async () => { - const { name: userName, entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - const { entityData } = await PostModel.get(postKey.name).populate('user'); - expect(entityData.user.id).equal(userKey.name); - expect(entityData.user.name).equal(userName); - assert.isUndefined(entityData.user.private); // make sure "read: false" is not leaked - }); - - it('should return "null" if trying to populate a prop that does not exist', async () => { - const { entityKey: postKey } = await addPost(); - - const { entityData } = await PostModel.get(postKey.name).populate('unknown'); - expect(entityData.unknown).equal(null); - }); - - it('should populate multiple props', async () => { - const { name: userName, entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - const { entityData } = await PostModel.get(postKey.name).populate(['user', 'publication', 'unknown']); - expect(entityData.user.name).equal(userName); - expect(entityData.publication).equal(null); - expect(entityData.unknown).equal(null); - }); - - it('should populate multiple props (2)', async () => { - const { name: userName, entityKey: userKey } = await addUser(); - const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); - const { entityKey: postKey } = await addPost(userKey, publicationKey); - - const { entityData } = await PostModel.get(postKey.name).populate(['user', 'publication']); - expect(entityData.user.name).equal(userName); - expect(entityData.publication.title).equal(publicationTitle); - }); - - it('should populate multiple props by **chaining** populate() calls', async () => { - const { name: userName, entityKey: userKey } = await addUser(); - const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); - const { entityKey: postKey } = await addPost(userKey, publicationKey); - - const { entityData } = await PostModel.get(postKey.name) - .populate('user') - .populate('publication'); - expect(entityData.user.name).equal(userName); - expect(entityData.publication.title).equal(publicationTitle); - }); - - it('should allow to select the properties to retrieve', async () => { - const { entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - const { entityData } = await PostModel.get(postKey.name).populate('user', ['email', 'private']); - assert.isDefined(entityData.user.email); - assert.isUndefined(entityData.user.name); - assert.isDefined(entityData.user.private); // force get private fields - }); - - it('should throw an error when providing multiple properties to populate + fields to select', async () => { - const { entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - try { - await PostModel.get(postKey.name).populate(['user', 'publication'], ['email', 'private']); - throw new Error('Shoud not get here.'); - } catch (err) { - expect(err.message).equal('Only 1 property can be populated when fields to select are provided'); - } - }); - - it('should populate multiple entities', async () => { - const { name: userName1, entityKey: userKey1 } = await addUser(); - const { name: userName2, entityKey: userKey2 } = await addUser(); - const { entityKey: postKey1 } = await addPost(userKey1); - const { entityKey: postKey2 } = await addPost(userKey2); - - const [post1, post2] = await PostModel.get([postKey1.name, postKey2.name]).populate('user'); - expect(post1.entityData.user.id).equal(userKey1.name); - expect(post1.entityData.user.name).equal(userName1); - expect(post2.entityData.user.id).equal(userKey2.name); - expect(post2.entityData.user.name).equal(userName2); - }); - - it('should allow nested embedded entities', async () => { - const { name: companyName, entityKey: companyKey } = await addCompany(); - const { name: userName, entityKey: userKey } = await addUser(companyKey); - const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); - const { entityKey: postKey } = await addPost(userKey, publicationKey); - - const { entityData } = await PostModel.get(postKey.name) - .populate(['user', 'user.company']) - .populate('publication') - .populate('publication.user') - .populate('publication.user.company') - .populate('path.that.does.not.exist'); - - expect(entityData.user.id).equal(userKey.name); - expect(entityData.user.name).equal(userName); - expect(entityData.user.company.name).equal(companyName); - expect(entityData.publication.title).equal(publicationTitle); - expect(entityData.user).deep.equal(entityData.publication.user); - expect(entityData.path.that.does.not.exist).equal(null); - }); - - it('should fetch all key references when no path is specified', async () => { - const { name: userName, entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - const { entityData } = await PostModel.get(postKey.name).populate(); - expect(entityData.user.name).equal(userName); - expect(entityData.publication).equal(null); - assert.isUndefined(entityData.user.private); // make sure "read: false" is not leaked - }); - - it('should fetch the keys inside a Transaction', async () => { - const transaction = gstore.transaction(); - sinon.spy(transaction, 'get'); - - const { name: userName, entityKey: userKey } = await addUser(); - const { entityKey: postKey } = await addPost(userKey); - - await transaction.run(); - const { entityData } = await PostModel.get(postKey.name, null, null, transaction).populate('user'); - await transaction.commit(); - expect(transaction.get.called).equal(true); - expect(transaction.get.callCount).equal(2); - expect(entityData.user.name).equal(userName); - }); - }); + const UserModel = gstore.model('ModelTests-User', userSchema); + const CompanyModel = gstore.model('ModelTests-Company', companySchema); + const PostModel = gstore.model('ModelTests-Post', postSchema); + const PublicationModel = gstore.model('ModelTests-Publication', publicationSchema); + + const addCompany = () => { + const name = randomName(); + const company = new CompanyModel({ name }); + return company.save().then(({ entityKey }) => { + addKey(entityKey); + return { name, entityKey }; + }); + }; + + const addUser = (company = null) => { + const name = randomName(); + const user = new UserModel( + { + name, + company, + email: chance.email(), + private: randomName(), + }, + randomName(), + ); + return user.save().then(({ entityKey }) => { + addKey(entityKey); + return { name, entityKey }; + }); + }; + + const addPost = (userKey = null, publicationKey = null) => { + const title = randomName(); + const post = new PostModel({ title, user: userKey, publication: publicationKey }, randomName()); + return post.save().then(({ entityKey }) => { + addKey(entityKey); + return { title, entityKey }; + }); + }; + + const addPublication = (userKey = null) => { + const title = randomName(); + const publication = new PublicationModel({ title, user: userKey }); + return publication.save().then(({ entityKey }) => { + addKey(entityKey); + return { title, entityKey }; + }); + }; + + describe('populate()', () => { + it('should fetch the "user" embedded entities', async () => { + const { name: userName, entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + const { entityData } = await PostModel.get(postKey.name).populate('user'); + expect(entityData.user.id).equal(userKey.name); + expect(entityData.user.name).equal(userName); + assert.isUndefined(entityData.user.private); // make sure "read: false" is not leaked + }); + + it('should return "null" if trying to populate a prop that does not exist', async () => { + const { entityKey: postKey } = await addPost(); + + const { entityData } = await PostModel.get(postKey.name).populate('unknown'); + expect(entityData.unknown).equal(null); + }); + + it('should populate multiple props', async () => { + const { name: userName, entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + const { entityData } = await PostModel.get(postKey.name).populate(['user', 'publication', 'unknown']); + expect(entityData.user.name).equal(userName); + expect(entityData.publication).equal(null); + expect(entityData.unknown).equal(null); + }); + + it('should populate multiple props (2)', async () => { + const { name: userName, entityKey: userKey } = await addUser(); + const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); + const { entityKey: postKey } = await addPost(userKey, publicationKey); + + const { entityData } = await PostModel.get(postKey.name).populate(['user', 'publication']); + expect(entityData.user.name).equal(userName); + expect(entityData.publication.title).equal(publicationTitle); + }); + + it('should populate multiple props by **chaining** populate() calls', async () => { + const { name: userName, entityKey: userKey } = await addUser(); + const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); + const { entityKey: postKey } = await addPost(userKey, publicationKey); + + const { entityData } = await PostModel.get(postKey.name) + .populate('user') + .populate('publication'); + expect(entityData.user.name).equal(userName); + expect(entityData.publication.title).equal(publicationTitle); + }); + + it('should allow to select the properties to retrieve', async () => { + const { entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + const { entityData } = await PostModel.get(postKey.name).populate('user', ['email', 'private']); + assert.isDefined(entityData.user.email); + assert.isUndefined(entityData.user.name); + assert.isDefined(entityData.user.private); // force get private fields + }); + + it('should throw an error when providing multiple properties to populate + fields to select', async () => { + const { entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + try { + await PostModel.get(postKey.name).populate(['user', 'publication'], ['email', 'private']); + throw new Error('Shoud not get here.'); + } catch (err) { + expect(err.message).equal('Only 1 property can be populated when fields to select are provided'); + } + }); + + it('should populate multiple entities', async () => { + const { name: userName1, entityKey: userKey1 } = await addUser(); + const { name: userName2, entityKey: userKey2 } = await addUser(); + const { entityKey: postKey1 } = await addPost(userKey1); + const { entityKey: postKey2 } = await addPost(userKey2); + + const [post1, post2] = await PostModel.get([postKey1.name, postKey2.name]).populate('user'); + expect(post1.entityData.user.id).equal(userKey1.name); + expect(post1.entityData.user.name).equal(userName1); + expect(post2.entityData.user.id).equal(userKey2.name); + expect(post2.entityData.user.name).equal(userName2); + }); + + it('should allow nested embedded entities', async () => { + const { name: companyName, entityKey: companyKey } = await addCompany(); + const { name: userName, entityKey: userKey } = await addUser(companyKey); + const { title: publicationTitle, entityKey: publicationKey } = await addPublication(userKey); + const { entityKey: postKey } = await addPost(userKey, publicationKey); + + const { entityData } = await PostModel.get(postKey.name) + .populate(['user', 'user.company']) + .populate('publication') + .populate('publication.user') + .populate('publication.user.company') + .populate('path.that.does.not.exist'); + + expect(entityData.user.id).equal(userKey.name); + expect(entityData.user.name).equal(userName); + expect(entityData.user.company.name).equal(companyName); + expect(entityData.publication.title).equal(publicationTitle); + expect(entityData.user).deep.equal(entityData.publication.user); + expect(entityData.path.that.does.not.exist).equal(null); + }); + + it('should fetch all key references when no path is specified', async () => { + const { name: userName, entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + const { entityData } = await PostModel.get(postKey.name).populate(); + expect(entityData.user.name).equal(userName); + expect(entityData.publication).equal(null); + assert.isUndefined(entityData.user.private); // make sure "read: false" is not leaked + }); + + it('should fetch the keys inside a Transaction', async () => { + const transaction = gstore.transaction(); + sinon.spy(transaction, 'get'); + + const { name: userName, entityKey: userKey } = await addUser(); + const { entityKey: postKey } = await addPost(userKey); + + await transaction.run(); + const { entityData } = await PostModel.get(postKey.name, null, null, transaction).populate('user'); + await transaction.commit(); + expect(transaction.get.called).equal(true); + expect(transaction.get.callCount).equal(2); + expect(entityData.user.name).equal(userName); + }); }); + }); + + describe('update()', () => { + describe('transaction()', () => { + const userSchema = new Schema( + { + name: { joi: Joi.string().required() }, + lastname: { joi: Joi.string() }, + password: { joi: Joi.string() }, + coins: { + joi: Joi.number() + .integer() + .min(0), + }, + email: { joi: Joi.string().email() }, + createdAt: { joi: Joi.date() }, + access_token: { joi: [Joi.string(), Joi.number()] }, + birthyear: { + joi: Joi.number() + .integer() + .min(1900) + .max(2013), + }, + }, + { joi: true }, + ); + + const User = gstore.model('ModelTestsTransaction-User', userSchema); + + it('should update entity inside a transaction', () => { + function transferCoins(fromUser, toUser, amount) { + return new Promise((resolve, reject) => { + const transaction = gstore.transaction(); + return transaction + .run() + .then(async () => { + await User.update( + fromUser.entityKey.name, + { + coins: fromUser.coins - amount, + }, + null, + null, + transaction, + ); + + await User.update( + toUser.entityKey.name, + { + coins: toUser.coins + amount, + }, + null, + null, + transaction, + ); + + transaction + .commit() + .then(async () => { + const [user1, user2] = await User.get( + [fromUser.entityKey.name, toUser.entityKey.name], + null, + null, + null, + { preserveOrder: true }, + ); + expect(user1.name).equal('User1'); + expect(user1.coins).equal(0); + expect(user2.name).equal('User2'); + expect(user2.coins).equal(1050); + resolve(); + }) + .catch(err => { + reject(err); + }); + }) + .catch(err => { + transaction.rollback(); + reject(err); + }); + }); + } - describe('update()', () => { - describe('transaction()', () => { - const userSchema = new Schema({ - name: { joi: Joi.string().required() }, - lastname: { joi: Joi.string() }, - password: { joi: Joi.string() }, - coins: { joi: Joi.number().integer().min(0) }, - email: { joi: Joi.string().email() }, - createdAt: { joi: Joi.date() }, - access_token: { joi: [Joi.string(), Joi.number()] }, - birthyear: { joi: Joi.number().integer().min(1900).max(2013) }, - }, { joi: true }); - - const User = gstore.model('ModelTestsTransaction-User', userSchema); - - it('should update entity inside a transaction', () => { - function transferCoins(fromUser, toUser, amount) { - return new Promise((resolve, reject) => { - const transaction = gstore.transaction(); - return transaction.run() - .then(async () => { - await User.update(fromUser.entityKey.name, { - coins: fromUser.coins - amount, - }, null, null, transaction); - - await User.update(toUser.entityKey.name, { - coins: toUser.coins + amount, - }, null, null, transaction); - - transaction.commit() - .then(async () => { - const [user1, user2] = await User.get([ - fromUser.entityKey.name, - toUser.entityKey.name, - ], null, null, null, { preserveOrder: true }); - expect(user1.name).equal('User1'); - expect(user1.coins).equal(0); - expect(user2.name).equal('User2'); - expect(user2.coins).equal(1050); - resolve(); - }).catch(err => { - reject(err); - }); - }).catch(err => { - transaction.rollback(); - reject(err); - }); - }); - } - - const fromUser = new User({ name: 'User1', coins: 1000 }, randomName()); - const toUser = new User({ name: 'User2', coins: 50 }, randomName()); - - return fromUser.save() - .then(({ entityKey }) => { - addKey(entityKey); - return toUser.save(); - }) - .then(({ entityKey }) => { - addKey(entityKey); - return transferCoins(fromUser, toUser, 1000); - }); - }); - - it('should throw a 404 Not found when trying to update a non existing entity', done => { - User.update(randomName(), { name: 'test' }) - .catch(err => { - expect(err.code).equal('ERR_ENTITY_NOT_FOUND'); - done(); - }); - }); + const fromUser = new User({ name: 'User1', coins: 1000 }, randomName()); + const toUser = new User({ name: 'User2', coins: 50 }, randomName()); + + return fromUser + .save() + .then(({ entityKey }) => { + addKey(entityKey); + return toUser.save(); + }) + .then(({ entityKey }) => { + addKey(entityKey); + return transferCoins(fromUser, toUser, 1000); + }); + }); + + it('should throw a 404 Not found when trying to update a non existing entity', done => { + User.update(randomName(), { name: 'test' }).catch(err => { + expect(err.code).equal('ERR_ENTITY_NOT_FOUND'); + done(); }); + }); }); - - describe('hooks', () => { - it('post delete hook should set scope on entity instance', () => { - const schema = new Schema({ name: { type: 'string' } }); - schema.post('delete', function postDelete({ key }) { - expect(key.kind).equal('ModelTests-Hooks'); - expect(key.id).equal(123); - expect(this instanceof Entity); - expect(key).equal(this.entityKey); - return Promise.resolve(); - }); - const Model = gstore.model('ModelTests-Hooks', schema); - return Model.delete(123); - }); + }); + + describe('hooks', () => { + it('post delete hook should set scope on entity instance', () => { + const schema = new Schema({ name: { type: 'string' } }); + schema.post('delete', function postDelete({ key }) { + expect(key.kind).equal('ModelTests-Hooks'); + expect(key.id).equal(123); + expect(this instanceof Entity.default); + expect(key).equal(this.entityKey); + return Promise.resolve(); + }); + const Model = gstore.model('ModelTests-Hooks', schema); + return Model.delete(123); }); + }); }); diff --git a/test/integration/query.js b/test/integration/query.js index defdabe..daebd4b 100644 --- a/test/integration/query.js +++ b/test/integration/query.js @@ -2,7 +2,6 @@ const chai = require('chai'); const Chance = require('chance'); -const { argv } = require('yargs'); const { Datastore } = require('@google-cloud/datastore'); const { Gstore } = require('../../lib'); @@ -15,10 +14,10 @@ gstoreWithCache.connect(ds); const { Schema } = gstore; const { expect, assert } = chai; const userSchema = new Schema({ - name: { type: String }, - age: { type: Number }, - address: { type: Schema.Types.Key }, - createdAt: { type: Date }, + name: { type: String }, + age: { type: Number }, + address: { type: Schema.Types.Key }, + createdAt: { type: Date }, }); const addressSchema = new Schema({ city: { type: String }, country: { type: String } }); const chance = new Chance(); @@ -30,309 +29,286 @@ const UserModel = gstore.model('QueryTests-User', userSchema); const AddressModel = gstore.model('QueryTests-Address', addressSchema); const getId = () => { - const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); - if (generatedIds.indexOf(id) >= 0) { - return getId(); - } - generatedIds.push(id); - return id; + const id = chance.string({ pool: 'abcdefghijklmnopqrstuvwxyz' }); + if (generatedIds.indexOf(id) >= 0) { + return getId(); + } + generatedIds.push(id); + return id; }; const getAddress = () => { - const key = AddressModel.key(getId()); - allKeys.push(key); - const data = { city: chance.city(), country: chance.country() }; - const address = new AddressModel(data, null, null, null, key); - return address; + const key = AddressModel.key(getId()); + allKeys.push(key); + const data = { city: chance.city(), country: chance.country() }; + const address = new AddressModel(data, null, null, null, key); + return address; }; const getUser = address => { - const key = UserModel.key(getId()); - allKeys.push(key); - const data = { - name: chance.string(), - age: chance.integer({ min: 1 }), - address: address.entityKey, - createdAt: new Date('2019-01-20'), - }; - const user = new UserModel(data, null, null, null, key); - return user; + const key = UserModel.key(getId()); + allKeys.push(key); + const data = { + name: chance.string(), + age: chance.integer({ min: 1 }), + address: address.entityKey, + createdAt: new Date('2019-01-20'), + }; + const user = new UserModel(data, null, null, null, key); + return user; }; const addresses = [getAddress(), getAddress(), getAddress(), getAddress()]; -const users = [ - getUser(addresses[0]), - getUser(addresses[1]), - getUser(addresses[2]), - getUser(addresses[3]), -]; -const mapAddressToId = addresses.reduce((acc, address) => ({ +const users = [getUser(addresses[0]), getUser(addresses[1]), getUser(addresses[2]), getUser(addresses[3])]; +const mapAddressToId = addresses.reduce( + (acc, address) => ({ ...acc, [address.entityKey.name]: address, -}), {}); + }), + {}, +); -const mapUserToId = users.reduce((acc, user) => ({ +const mapUserToId = users.reduce( + (acc, user) => ({ ...acc, [user.entityKey.name]: user, -}), {}); - -const cleanUp = () => ds.delete(allKeys).then(() => Promise.all([UserModel.deleteAll(), AddressModel.deleteAll()])) + }), + {}, +); + +const cleanUp = () => + ds + .delete(allKeys) + .then(() => Promise.all([UserModel.deleteAll(), AddressModel.deleteAll()])) .catch(err => { console.log('Error cleaning up'); // eslint-disable-line console.log(err); // eslint-disable-line }); describe('Queries (Integration Tests)', () => { - before(function integrationTest() { - generatedIds = []; - if (argv.int !== true) { - this.skip(); - } - return gstore.save([...users, ...addresses]); - }); + before(() => { + generatedIds = []; + return gstore.save([...users, ...addresses]); + }); + + after(() => cleanUp()); + + describe('Setup', () => { + it('Return all the User and Addresses entities', () => + UserModel.query() + .run() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + }) + .then(() => AddressModel.query().run()) + .then(({ entities }) => { + expect(entities.length).equal(addresses.length); + })); + }); + + describe('list()', () => { + describe('populate()', () => { + it('should populate the address of all users', () => + UserModel.list() + .populate() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + + it('should also work with ENTITY format', () => + UserModel.list({ format: 'ENTITY' }) + .populate() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const { entityKey } = entity; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + + it('should allow to select specific reference entity fields', () => + UserModel.list() + .populate('address', 'country') + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.country).equal(address.country); + assert.isUndefined(entity.address.city); + }); + })); - after(function afterAllIntTest() { - if (argv.int !== true) { - this.skip(); - } - return cleanUp(); - }); + context('when cache is active', () => { + before(() => { + gstore.cache = gstoreWithCache.cache; + }); + after(() => { + delete gstore.cache; + }); + afterEach(() => { + gstore.cache.reset(); + }); - beforeEach(function integrationTest() { - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } + it('should also populate() fields', () => + UserModel.list() + .populate() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + }); }); - - describe('Setup', () => { - it('Return all the User and Addresses entities', () => ( - UserModel.query().run() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - }) - .then(() => AddressModel.query().run()) - .then(({ entities }) => { - expect(entities.length).equal(addresses.length); - }) - )); + }); + + describe('findOne()', () => { + describe('populate()', () => { + it('should populate the address of all users', () => + UserModel.findOne({ name: users[0].name }) + .populate() + .then(entity => { + const addressId = mapUserToId[entity.entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + })); + + it('should allow to select specific reference entity fields', () => + UserModel.findOne({ name: users[0].name }) + .populate('address', 'country') + .then(entity => { + const addressId = mapUserToId[entity.entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.country).equal(address.country); + assert.isUndefined(entity.address.city); + })); }); - - describe('list()', () => { - describe('populate()', () => { - it('should populate the address of all users', () => ( - UserModel.list() - .populate() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should also work with ENTITY format', () => ( - UserModel.list({ format: 'ENTITY' }) - .populate() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const { entityKey } = entity; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should allow to select specific reference entity fields', () => ( - UserModel.list() - .populate('address', 'country') - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.country).equal(address.country); - assert.isUndefined(entity.address.city); - }); - }) - )); - - context('when cache is active', () => { - before(() => { - gstore.cache = gstoreWithCache.cache; - }); - after(() => { - delete gstore.cache; - }); - afterEach(() => { - gstore.cache.reset(); - }); - - it('should also populate() fields', () => ( - UserModel.list() - .populate() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); + }); + + describe('findAround()', () => { + describe('populate()', () => { + it('should populate the address of all users', () => + UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10 }) + .populate() + .then(entities => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); }); - }); - }); - - describe('findOne()', () => { - describe('populate()', () => { - it('should populate the address of all users', () => ( - UserModel.findOne({ name: users[0].name }) - .populate() - .then(entity => { - const addressId = mapUserToId[entity.entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }) - )); - - it('should allow to select specific reference entity fields', () => ( - UserModel.findOne({ name: users[0].name }) - .populate('address', 'country') - .then(entity => { - const addressId = mapUserToId[entity.entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.country).equal(address.country); - assert.isUndefined(entity.address.city); - }) - )); - }); - }); - - describe('findAround()', () => { - describe('populate()', () => { - it('should populate the address of all users', () => ( - UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10 }) - .populate() - .then(entities => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should also work with ENTITY format', () => ( - UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10, format: 'ENTITY' }) - .populate() - .then(entities => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const { entityKey } = entity; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should allow to select specific reference entity fields', () => ( - UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10 }) - .populate('address', 'country') - .then(entities => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.country).equal(address.country); - assert.isUndefined(entity.address.city); - }); - }) - )); - }); + })); + + it('should also work with ENTITY format', () => + UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10, format: 'ENTITY' }) + .populate() + .then(entities => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const { entityKey } = entity; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + + it('should allow to select specific reference entity fields', () => + UserModel.findAround('createdAt', new Date('2019-01-01'), { after: 10 }) + .populate('address', 'country') + .then(entities => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.country).equal(address.country); + assert.isUndefined(entity.address.city); + }); + })); }); - - describe('datastore Queries()', () => { - describe('populate()', () => { - it('should populate the address of all users', () => ( - UserModel.query() - .filter('createdAt', '>', new Date('2019-01-01')) - .run() - .populate() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should also work with ENTITY format', () => ( - UserModel.query() - .filter('createdAt', '>', new Date('2019-01-01')) - .run({ format: 'ENTITY' }) - .populate() - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const { entityKey } = entity; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.city).equal(address.city); - expect(entity.address.country).equal(address.country); - }); - }) - )); - - it('should allow to select specific reference entity fields', () => ( - UserModel.query() - .filter('createdAt', '>', new Date('2019-01-01')) - .run() - .populate('address', 'country') - .populate('unknown') - .then(({ entities }) => { - expect(entities.length).equal(users.length); - - entities.forEach(entity => { - const entityKey = entity[gstore.ds.KEY]; - const addressId = mapUserToId[entityKey.name].address.name; - const address = mapAddressToId[addressId]; - expect(entity.address.country).equal(address.country); - expect(entity.unknown).equal(null); - assert.isUndefined(entity.address.city); - }); - }) - )); - }); + }); + + describe('datastore Queries()', () => { + describe('populate()', () => { + it('should populate the address of all users', () => + UserModel.query() + .filter('createdAt', '>', new Date('2019-01-01')) + .run() + .populate() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + + it('should also work with ENTITY format', () => + UserModel.query() + .filter('createdAt', '>', new Date('2019-01-01')) + .run({ format: 'ENTITY' }) + .populate() + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const { entityKey } = entity; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.city).equal(address.city); + expect(entity.address.country).equal(address.country); + }); + })); + + it('should allow to select specific reference entity fields', () => + UserModel.query() + .filter('createdAt', '>', new Date('2019-01-01')) + .run() + .populate('address', 'country') + .populate('unknown') + .then(({ entities }) => { + expect(entities.length).equal(users.length); + + entities.forEach(entity => { + const entityKey = entity[gstore.ds.KEY]; + const addressId = mapUserToId[entityKey.name].address.name; + const address = mapAddressToId[addressId]; + expect(entity.address.country).equal(address.country); + expect(entity.unknown).equal(null); + assert.isUndefined(entity.address.city); + }); + })); }); + }); }); diff --git a/test/integration/schema.js b/test/integration/schema.js index cca748c..3e55e5b 100644 --- a/test/integration/schema.js +++ b/test/integration/schema.js @@ -5,7 +5,6 @@ const chai = require('chai'); const Chance = require('chance'); const { Datastore } = require('@google-cloud/datastore'); -const { argv } = require('yargs'); const { Gstore } = require('../../lib'); const gstore = new Gstore(); @@ -17,57 +16,55 @@ const { expect } = chai; const { Schema } = gstore; describe('Schema (Integration Tests)', () => { - beforeEach(function integrationTest() { - gstore.models = {}; - gstore.modelSchemas = {}; + beforeEach(() => { + gstore.models = {}; + gstore.modelSchemas = {}; + }); - if (argv.int !== true) { - // Skip e2e tests suite - this.skip(); - } + it('read param set to "false" should not return those properties from entity.plain()', () => { + const schema = new Schema({ + email: { + type: String, + required: true, + }, + password: { + type: String, + validate: { + rule: 'isLength', + args: [{ min: 8, max: undefined }], + }, + required: true, + read: false, + excludeFromIndexes: true, + }, + state: { + type: String, + default: 'requested', + write: false, + read: false, + excludeFromIndexes: true, + }, }); - it('read param set to "false" should not return those properties from entity.plain()', () => { - const schema = new Schema({ - email: { - type: String, - required: true, - }, - password: { - type: String, - validate: { - rule: 'isLength', - args: [{ min: 8, max: undefined }], - }, - required: true, - read: false, - excludeFromIndexes: true, - }, - state: { - type: String, - default: 'requested', - write: false, - read: false, - excludeFromIndexes: true, - }, - }); + const User = gstore.model('ModelTestsSchema-User', schema); - const User = gstore.model('ModelTestsSchema-User', schema); + const email = chance.email(); + const password = chance.string({ length: 10 }); + const user = new User({ email, password }); - const email = chance.email(); - const password = chance.string({ length: 10 }); - const user = new User({ email, password }); + return user + .save() + .then(entity => { + const response = entity.plain(); + expect(response.password).to.not.exist; + expect(response.requested).to.not.exist; - return user.save().then(entity => { - const response = entity.plain(); - expect(response.password).to.not.exist; - expect(response.requested).to.not.exist; - - const response2 = entity.plain({ readAll: true }); - expect(response2.password).equal(password); - expect(response2.state).equal('requested'); - }).catch(err => { - throw (err); - }); - }); + const response2 = entity.plain({ readAll: true }); + expect(response2.password).equal(password); + expect(response2.state).equal('requested'); + }) + .catch(err => { + throw err; + }); + }); }); diff --git a/test/mocks/datastore.js b/test/mocks/datastore.js index fdf242c..fd8167e 100644 --- a/test/mocks/datastore.js +++ b/test/mocks/datastore.js @@ -3,69 +3,69 @@ const { Datastore: GoogleDatastore } = require('@google-cloud/datastore'); class Datastore { - constructor(options) { - this.googleDatastore = new GoogleDatastore(options); - } + constructor(options) { + this.googleDatastore = new GoogleDatastore(options); + } - key(options) { - return this.googleDatastore.key(options); - } + key(options) { + return this.googleDatastore.key(options); + } - isKey(key) { - return this.googleDatastore.isKey(key); - } + isKey(key) { + return this.googleDatastore.isKey(key); + } - save() { - return Promise.resolve(this); - } + save() { + return Promise.resolve(this); + } - get() { - return Promise.resolve(this); - } + get() { + return Promise.resolve(this); + } - delete() { - return Promise.resolve(this); - } + delete() { + return Promise.resolve(this); + } - createQuery() { - return this.googleDatastore.createQuery.apply(this.googleDatastore, arguments); - } + createQuery() { + return this.googleDatastore.createQuery.apply(this.googleDatastore, arguments); + } - runQuery() { - return Promise.resolve([[], { moreResults: 'MORE_RESULT', __ref: this }]); - } + runQuery() { + return Promise.resolve([[], { moreResults: 'MORE_RESULT', __ref: this }]); + } - transaction() { - return { __ref: this }; - } + transaction() { + return { __ref: this }; + } - int() { - return this.googleDatastore.int.apply(this.googleDatastore, arguments); - } + int() { + return this.googleDatastore.int.apply(this.googleDatastore, arguments); + } - double() { - return this.googleDatastore.double.apply(this.googleDatastore, arguments); - } + double() { + return this.googleDatastore.double.apply(this.googleDatastore, arguments); + } - geoPoint() { - return this.googleDatastore.geoPoint.apply(this.googleDatastore, arguments); - } + geoPoint() { + return this.googleDatastore.geoPoint.apply(this.googleDatastore, arguments); + } - get MORE_RESULTS_AFTER_LIMIT() { - return this.googleDatastore.MORE_RESULTS_AFTER_LIMIT; - } + get MORE_RESULTS_AFTER_LIMIT() { + return this.googleDatastore.MORE_RESULTS_AFTER_LIMIT; + } - get MORE_RESULTS_AFTER_CURSOR() { - return this.googleDatastore.MORE_RESULTS_AFTER_CURSOR; - } + get MORE_RESULTS_AFTER_CURSOR() { + return this.googleDatastore.MORE_RESULTS_AFTER_CURSOR; + } - get NO_MORE_RESULTS() { - return this.googleDatastore.NO_MORE_RESULTS; - } + get NO_MORE_RESULTS() { + return this.googleDatastore.NO_MORE_RESULTS; + } - get KEY() { - return this.googleDatastore.KEY; - } + get KEY() { + return this.googleDatastore.KEY; + } } module.exports = options => new Datastore(options); diff --git a/test/mocks/entities.js b/test/mocks/entities.js index 8fddc65..f4767e4 100644 --- a/test/mocks/entities.js +++ b/test/mocks/entities.js @@ -1,7 +1,7 @@ 'use strict'; const ds = require('./datastore')({ - namespace: 'com.mydomain', + namespace: 'com.mydomain', }); const key1 = ds.key(['User', 111]); @@ -26,32 +26,32 @@ entity5[ds.KEY] = key5; // to make sure we did not mutate any entity in our // tests... We should not, but who knows? const generateEntities = () => { - const mockEntity = { - name: 'John', - lastname: 'Snow', - email: 'john@snow.com', - }; + const mockEntity = { + name: 'John', + lastname: 'Snow', + email: 'john@snow.com', + }; - mockEntity[ds.KEY] = ds.key(['BlogPost', 1234]); + mockEntity[ds.KEY] = ds.key(['BlogPost', 1234]); - const mockEntity2 = { name: 'John', lastname: 'Snow', password: 'xxx' }; - mockEntity2[ds.KEY] = ds.key(['BlogPost', 1234]); + const mockEntity2 = { name: 'John', lastname: 'Snow', password: 'xxx' }; + mockEntity2[ds.KEY] = ds.key(['BlogPost', 1234]); - const mockEntity3 = { name: 'Mick', lastname: 'Jagger' }; - mockEntity3[ds.KEY] = ds.key(['BlogPost', 'keyname']); + const mockEntity3 = { name: 'Mick', lastname: 'Jagger' }; + mockEntity3[ds.KEY] = ds.key(['BlogPost', 'keyname']); - const mockEntities = [mockEntity2, mockEntity3]; + const mockEntities = [mockEntity2, mockEntity3]; - return { - mockEntity, - mockEntity2, - mockEntity3, - mockEntities, - }; + return { + mockEntity, + mockEntity2, + mockEntity3, + mockEntities, + }; }; module.exports = { - keys: [key1, key2, key3, key4, key5], - entities: [entity1, entity2, entity3, entity4, entity5], - generateEntities, + keys: [key1, key2, key3, key4, key5], + entities: [entity1, entity2, entity3, entity4, entity5], + generateEntities, }; diff --git a/test/mocks/query.js b/test/mocks/query.js index c014e4c..f2a6049 100644 --- a/test/mocks/query.js +++ b/test/mocks/query.js @@ -1,37 +1,49 @@ 'use strict'; class Query { - constructor(ds, mocks, info) { - this.ds = ds; - this.mocks = mocks; - this.info = info; - this.kinds = ['MockQuery']; - this.filters = []; - this.namespace = 'mock.query'; - this.groupByVal = []; - this.orders = []; - this.selectVal = []; - } - - run() { - const info = this.info || { - moreResults: this.ds.MORE_RESULTS_AFTER_LIMIT, - endCursor: 'abcdef', - }; - return Promise.resolve([this.mocks.entities, info]); - } - - limit() { return this; } - - offset() { return this; } - - order() { return this; } - - filter() { return this; } - - select() { return this; } - - hasAncestor(ancestors) { this.ancestors = ancestors; } + constructor(ds, mocks, info, namespace) { + this.ds = ds; + this.mocks = mocks; + this.info = info; + this.kinds = ['MockQuery']; + this.filters = []; + this.namespace = namespace || 'mock.query'; + this.groupByVal = []; + this.orders = []; + this.selectVal = []; + } + + run() { + const info = this.info || { + moreResults: this.ds.MORE_RESULTS_AFTER_LIMIT, + endCursor: 'abcdef', + }; + return Promise.resolve([this.mocks.entities, info]); + } + + limit() { + return this; + } + + offset() { + return this; + } + + order() { + return this; + } + + filter() { + return this; + } + + select() { + return this; + } + + hasAncestor(ancestors) { + this.ancestors = ancestors; + } } module.exports = Query; diff --git a/test/mocks/transaction.js b/test/mocks/transaction.js index 19eb484..a780dfa 100644 --- a/test/mocks/transaction.js +++ b/test/mocks/transaction.js @@ -1,18 +1,18 @@ 'use strict'; function Transaction() { - const _this = this; - this.run = () => { }; - this.get = () => { }; - this.save = () => { }; - this.delete = () => { }; - this.commit = () => Promise.resolve(); - this.rollback = () => Promise.resolve(); - this.createQuery = () => ({ - filter: () => { }, - scope: _this, - }); - this.runQuery = () => Promise.resolve(); + const _this = this; + this.run = () => {}; + this.get = () => {}; + this.save = () => {}; + this.delete = () => {}; + this.commit = () => Promise.resolve(); + this.rollback = () => Promise.resolve(); + this.createQuery = () => ({ + filter: () => {}, + scope: _this, + }); + this.runQuery = () => Promise.resolve(); } Transaction.prototype.name = 'Transaction'; diff --git a/test/model-test.js b/test/model-test.js index fde7091..8f645ac 100755 --- a/test/model-test.js +++ b/test/model-test.js @@ -1,4 +1,3 @@ - 'use strict'; const chai = require('chai'); @@ -8,8 +7,7 @@ const Joi = require('@hapi/joi'); const { Gstore } = require('../lib'); const Entity = require('../lib/entity'); -const Model = require('../lib/model'); -const gstoreErrors = require('../lib/errors'); +const { ERROR_CODES } = require('../lib/errors'); const ds = require('./mocks/datastore')({ namespace: 'com.mydomain' }); const Transaction = require('./mocks/transaction'); const { generateEntities } = require('./mocks/entities'); @@ -18,1528 +16,1481 @@ const Query = require('./mocks/query'); const gstore = new Gstore(); const gstoreWithCache = new Gstore({ cache: { config: { ttl: { queries: 600 } } } }); const { expect, assert } = chai; -const { Schema, createDataLoader } = gstore; +const { Schema } = gstore; describe('Model', () => { - let schema; - let GstoreModel; - let mockEntity; - let mockEntities; - let transaction; - - beforeEach('Before each Model (global)', () => { - gstore.models = {}; - gstore.modelSchemas = {}; - gstore.options = {}; - gstore.cache = undefined; - gstore.config.errorOnEntityNotFound = true; - - gstore.connect(ds); - gstoreWithCache.connect(ds); - - schema = new Schema({ - name: { type: String }, - lastname: { type: String, excludeFromIndexes: true }, - password: { read: false }, - age: { type: 'int', excludeFromIndexes: true }, - birthday: { type: 'datetime' }, - street: {}, - website: { validate: 'isURL' }, - email: { validate: 'isEmail' }, - ip: { validate: { rule: 'isIP', args: [4] } }, - ip2: { validate: { rule: 'isIP' } }, // no args passed - modified: { type: 'boolean' }, - tags: { type: 'array' }, - prefs: { type: 'object' }, - price: { type: 'double', write: false }, - icon: { type: 'buffer' }, - location: { type: 'geoPoint' }, - }); - schema.virtual('fullname').get(() => { }); - - ({ mockEntity, mockEntities } = generateEntities()); - transaction = new Transaction(); - - sinon.spy(ds, 'save'); - sinon.stub(ds, 'transaction').callsFake(() => transaction); - sinon.spy(transaction, 'save'); - sinon.spy(transaction, 'commit'); - sinon.spy(transaction, 'rollback'); - sinon.stub(transaction, 'get').resolves([mockEntity]); - sinon.stub(transaction, 'run').resolves([transaction, { apiData: 'ok' }]); - - GstoreModel = gstore.model('Blog', schema, gstore); + let schema; + let GstoreModel; + let mockEntity; + let mockEntities; + let transaction; + + beforeEach('Before each Model (global)', () => { + gstore.models = {}; + gstore.modelSchemas = {}; + gstore.options = {}; + gstore.cache = undefined; + gstore.config.errorOnEntityNotFound = true; + + gstore.connect(ds); + gstoreWithCache.connect(ds); + + schema = new Schema({ + name: { type: String }, + lastname: { type: String, excludeFromIndexes: true }, + password: { read: false }, + age: { type: 'int', excludeFromIndexes: true }, + birthday: { type: 'datetime' }, + street: {}, + website: { validate: 'isURL' }, + email: { validate: 'isEmail' }, + ip: { validate: { rule: 'isIP', args: [4] } }, + ip2: { validate: { rule: 'isIP' } }, // no args passed + modified: { type: 'boolean' }, + tags: { type: 'array' }, + prefs: { type: 'object' }, + price: { type: 'double', write: false }, + icon: { type: 'buffer' }, + location: { type: 'geoPoint' }, + }); + schema.virtual('fullname').get(() => {}); + + ({ mockEntity, mockEntities } = generateEntities()); + transaction = new Transaction(); + + sinon.spy(ds, 'save'); + sinon.stub(ds, 'transaction').callsFake(() => transaction); + sinon.spy(transaction, 'save'); + sinon.spy(transaction, 'commit'); + sinon.spy(transaction, 'rollback'); + sinon.stub(transaction, 'get').resolves([mockEntity]); + sinon.stub(transaction, 'run').resolves([transaction, { apiData: 'ok' }]); + + GstoreModel = gstore.model('Blog', schema, gstore); + }); + + afterEach(() => { + ds.save.restore(); + ds.transaction.restore(); + transaction.save.restore(); + transaction.commit.restore(); + transaction.rollback.restore(); + }); + + describe('compile()', () => { + beforeEach('Reset before compile', () => { + gstore.models = {}; + gstore.modelSchemas = {}; + GstoreModel = gstore.model('Blog', schema); }); - afterEach(() => { - ds.save.restore(); - ds.transaction.restore(); - transaction.save.restore(); - transaction.commit.restore(); - transaction.rollback.restore(); - }); - - describe('compile()', () => { - beforeEach('Reset before compile', () => { - gstore.models = {}; - gstore.modelSchemas = {}; - GstoreModel = gstore.model('Blog', schema); - }); - - it('should set properties on compile and return GstoreModel', () => { - assert.isDefined(GstoreModel.schema); - assert.isDefined(GstoreModel.gstore); - assert.isDefined(GstoreModel.entityKind); - }); - - it('should create new models classes', () => { - const User = Model.compile('User', new Schema({}), gstore); - - expect(User.entityKind).equal('User'); - expect(GstoreModel.entityKind).equal('Blog'); - }); - - it('should execute methods passed to schema.methods', () => { - const imageSchema = new Schema({}); - const ImageModel = gstore.model('Image', imageSchema); - sinon.stub(ImageModel, 'get').callsFake((id, cb) => { - cb(null, mockEntities[0]); - }); - schema.methods.fullName = function fullName(cb) { - return cb(null, `${this.get('name')} ${this.get('lastname')}`); - }; - schema.methods.getImage = function getImage(cb) { - return this.model('Image').get(this.entityData.imageIdx, cb); - }; - - GstoreModel = gstore.model('MyEntity', schema); - const entity = new GstoreModel({ name: 'John', lastname: 'Snow' }); - - entity.fullName((err, result) => { - expect(result).equal('John Snow'); - }); - - entity.getImage.call(entity, (err, result) => { - expect(result).equal(mockEntities[0]); - }); - }); - - it('should execute static methods', () => { - schema = new Schema({}); - schema.statics.doSomething = () => 123; - - GstoreModel = gstore.model('MyEntity', schema); - - expect(GstoreModel.doSomething()).equal(123); - }); - - it('should throw error is trying to override reserved methods', () => { - schema = new Schema({}); - - schema.statics.get = () => 123; - const fn = () => gstore.model('MyEntity', schema); + it('should set properties on compile and return GstoreModel', () => { + assert.isDefined(GstoreModel.schema); + assert.isDefined(GstoreModel.gstore); + assert.isDefined(GstoreModel.entityKind); + }); - expect(fn).throw(Error); - }); + it('should create new models classes', () => { + const User = gstore.model('User', new Schema({})); - it('should add __meta object', () => { - GstoreModel = gstore.model('MyEntity', schema); + expect(User.entityKind).equal('User'); + expect(GstoreModel.entityKind).equal('Blog'); + }); - assert.isDefined(GstoreModel.schema.__meta); - expect(GstoreModel.schema.__meta.geoPointsProps).deep.equal(['location']); - }); + it('should execute methods passed to schema.methods', () => { + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); + sinon.stub(ImageModel, 'get').callsFake((id, cb) => { + cb(null, mockEntities[0]); + }); + schema.methods.fullName = function fullName(cb) { + return cb(null, `${this.get('name')} ${this.get('lastname')}`); + }; + schema.methods.getImage = function getImage(cb) { + return this.model('Image').get(this.entityData.imageIdx, cb); + }; + + GstoreModel = gstore.model('MyEntity', schema); + const entity = new GstoreModel({ name: 'John', lastname: 'Snow' }); + + entity.fullName((err, result) => { + expect(result).equal('John Snow'); + }); + + entity.getImage.call(entity, (err, result) => { + expect(result).equal(mockEntities[0]); + }); }); - describe('sanitize()', () => { - it('should remove keys not "writable"', () => { - let data = { price: 20, unknown: 'hello', name: 'John' }; + it('should add __meta object', () => { + GstoreModel = gstore.model('MyEntity', schema); - data = GstoreModel.sanitize(data); + assert.isDefined(GstoreModel.schema.__meta); + expect(GstoreModel.schema.__meta.geoPointsProps).deep.equal(['location']); + }); + }); - assert.isUndefined(data.price); - assert.isUndefined(data.unknown); - }); + describe('sanitize()', () => { + it('should remove keys not "writable"', () => { + let data = { price: 20, unknown: 'hello', name: 'John' }; - it('should convert "null" string to null', () => { - let data = { - name: 'null', - }; + data = GstoreModel.sanitize(data); - data = GstoreModel.sanitize(data); + assert.isUndefined(data.price); + assert.isUndefined(data.unknown); + }); - expect(data.name).equal(null); - }); + it('should convert "null" string to null', () => { + let data = { + name: 'null', + }; - it('return null if data is not an object', () => { - let data = 'hello'; + data = GstoreModel.sanitize(data); - data = GstoreModel.sanitize(data); + expect(data.name).equal(null); + }); - expect(data).equal(null); - }); + it('return an empty object if data is not an object', () => { + let data = 'hello'; - it('should not mutate the entityData passed', () => { - const data = { name: 'John' }; - const data2 = GstoreModel.sanitize(data); + data = GstoreModel.sanitize(data); - expect(data2).not.equal(data); - }); + expect(data).deep.equal({}); + }); - it('should remove not writable & unknown props in Joi schema', () => { - schema = new Schema({ - createdOn: { joi: Joi.date(), write: false }, - }, { joi: true }); - GstoreModel = gstore.model('BlogJoi', schema, gstore); + it('should not mutate the entityData passed', () => { + const data = { name: 'John' }; + const data2 = GstoreModel.sanitize(data); - const entityData = GstoreModel.sanitize({ createdOn: Date.now(), unknown: 123 }); + expect(data2).not.equal(data); + }); - assert.isUndefined(entityData.createdOn); - assert.isUndefined(entityData.unknown); - }); + it('should remove not writable & unknown props in Joi schema', () => { + schema = new Schema( + { + createdOn: { joi: Joi.date(), write: false }, + }, + { joi: true }, + ); + GstoreModel = gstore.model('BlogJoi', schema, gstore); - it('should *not* remove unknown props in Joi schema', () => { - schema = new Schema({ - createdOn: { joi: Joi.date(), write: false }, - }, { joi: { options: { allowUnknown: true } } }); - GstoreModel = gstore.model('BlogJoi', schema, gstore); + const entityData = GstoreModel.sanitize({ createdOn: Date.now(), unknown: 123 }); - const entityData = GstoreModel.sanitize({ createdOn: Date.now(), unknown: 123 }); + assert.isUndefined(entityData.createdOn); + assert.isUndefined(entityData.unknown); + }); - assert.isDefined(entityData.unknown); - }); + it('should *not* remove unknown props in Joi schema', () => { + schema = new Schema( + { + createdOn: { joi: Joi.date(), write: false }, + }, + { joi: { options: { allowUnknown: true } } }, + ); + GstoreModel = gstore.model('BlogJoi', schema, gstore); - it('should return the same value object from Model.sanitize and Entity.validate in Joi schema', () => { - schema = new Schema({ - foo: { joi: Joi.object({ bar: Joi.any() }).required() }, - createdOn: { joi: Joi.date().default(() => new Date('01-01-2019'), 'static createdOn date') }, - }, { joi: true }); - GstoreModel = gstore.model('BlogJoi', schema, gstore); + const entityData = GstoreModel.sanitize({ createdOn: Date.now(), unknown: 123 }); - const data = { foo: { unknown: 123 } }; - const entityData = GstoreModel.sanitize(data); - const { value: validationData, error: validationError } = new GstoreModel(data).validate(); + assert.isDefined(entityData.unknown); + }); - assert.isUndefined(entityData.foo.unknown); - assert.isNull(validationError); - assert.deepEqual(entityData, validationData); - }); + it('should return the same value object from Model.sanitize and Entity.validate in Joi schema', () => { + schema = new Schema( + { + foo: { joi: Joi.object({ bar: Joi.any() }).required() }, + createdOn: { joi: Joi.date().default(() => new Date('01-01-2019'), 'static createdOn date') }, + }, + { joi: true }, + ); + GstoreModel = gstore.model('BlogJoi', schema, gstore); + + const data = { foo: { unknown: 123 } }; + const entityData = GstoreModel.sanitize(data); + const { value: validationData, error: validationError } = new GstoreModel(data).validate(); + + assert.isUndefined(entityData.foo.unknown); + assert.isNull(validationError); + assert.deepEqual(entityData, validationData); + }); - it('should preserve the datastore.KEY', () => { - const key = GstoreModel.key(123); - let data = { foo: 'bar' }; - data[GstoreModel.gstore.ds.KEY] = key; + it('should preserve the datastore.KEY', () => { + const key = GstoreModel.key(123); + let data = { foo: 'bar' }; + data[GstoreModel.gstore.ds.KEY] = key; - data = GstoreModel.sanitize(data); + data = GstoreModel.sanitize(data); - expect(data[GstoreModel.gstore.ds.KEY]).to.equal(key); - }); + expect(data[GstoreModel.gstore.ds.KEY]).to.equal(key); + }); - it('should preserve the datastore.KEY with Joi Schemas', () => { - schema = new Schema({}, { joi: true }); - GstoreModel = gstore.model('SanitizeJoiSchemaPreserveKEY', schema, gstore); - const key = GstoreModel.key(123); - const data = { foo: 'bar' }; - data[GstoreModel.gstore.ds.KEY] = key; + it('should preserve the datastore.KEY with Joi Schemas', () => { + schema = new Schema({}, { joi: true }); + GstoreModel = gstore.model('SanitizeJoiSchemaPreserveKEY', schema, gstore); + const key = GstoreModel.key(123); + const data = { foo: 'bar' }; + data[GstoreModel.gstore.ds.KEY] = key; - const sanitized = GstoreModel.sanitize(data); + const sanitized = GstoreModel.sanitize(data); - expect(sanitized[gstore.ds.KEY]).to.equal(key); - }); + expect(sanitized[gstore.ds.KEY]).to.equal(key); + }); - describe('populated entities', () => { - beforeEach(() => { - schema = new Schema({ ref: { type: Schema.Types.Key } }); - GstoreModel = gstore.model('SanitizeReplacePopulatedEntity', schema, gstore); - }); + describe('populated entities', () => { + beforeEach(() => { + schema = new Schema({ ref: { type: Schema.Types.Key } }); + GstoreModel = gstore.model('SanitizeReplacePopulatedEntity', schema, gstore); + }); - it('should replace a populated entity ref with its entity key', () => { - const key = GstoreModel.key('abc'); - const data = { - ref: { - title: 'Entity title populated', - [gstore.ds.KEY]: key, - }, - }; + it('should replace a populated entity ref with its entity key', () => { + const key = GstoreModel.key('abc'); + const data = { + ref: { + title: 'Entity title populated', + [gstore.ds.KEY]: key, + }, + }; - const sanitized = GstoreModel.sanitize(data); + const sanitized = GstoreModel.sanitize(data); - assert.isTrue(gstore.ds.isKey(sanitized.ref)); - expect(sanitized.ref).to.equal(key); - }); + assert.isTrue(gstore.ds.isKey(sanitized.ref)); + expect(sanitized.ref).to.equal(key); + }); - it('should not replace a ref that is not an object', () => { - const data = { ref: null }; + it('should not replace a ref that is not an object', () => { + const data = { ref: null }; - const sanitized = GstoreModel.sanitize(data); + const sanitized = GstoreModel.sanitize(data); - assert.isFalse(gstore.ds.isKey(sanitized.ref)); - expect(sanitized.ref).to.equal(null); - }); - }); + assert.isFalse(gstore.ds.isKey(sanitized.ref)); + expect(sanitized.ref).to.equal(null); + }); }); + }); - describe('key()', () => { - it('should create from entityKind', () => { - const key = GstoreModel.key(); + describe('key()', () => { + it('should create from entityKind', () => { + const key = GstoreModel.key(); - expect(key.path[0]).equal('Blog'); - assert.isUndefined(key.path[1]); - }); + expect(key.path[0]).equal('Blog'); + assert.isUndefined(key.path[1]); + }); - it('should create array of ids', () => { - const keys = GstoreModel.key([22, 69]); + it('should create array of ids', () => { + const keys = GstoreModel.key([22, 69]); - expect(is.array(keys)).equal(true); - expect(keys.length).equal(2); - expect(keys[1].path[1]).equal(69); - }); + expect(is.array(keys)).equal(true); + expect(keys.length).equal(2); + expect(keys[1].path[1]).equal(69); + }); - it('should create array of ids with ancestors and namespace', () => { - const namespace = 'com.mydomain-dev'; - const keys = GstoreModel.key([22, 69], ['Parent', 'keyParent'], namespace); + it('should create array of ids with ancestors and namespace', () => { + const namespace = 'com.mydomain-dev'; + const keys = GstoreModel.key([22, 69], ['Parent', 'keyParent'], namespace); - expect(keys[0].path[0]).equal('Parent'); - expect(keys[0].path[1]).equal('keyParent'); - expect(keys[1].namespace).equal(namespace); - }); + expect(keys[0].path[0]).equal('Parent'); + expect(keys[0].path[1]).equal('keyParent'); + expect(keys[1].namespace).equal(namespace); }); + }); - describe('get()', () => { - let entity; - - beforeEach(() => { - entity = { name: 'John' }; - entity[ds.KEY] = GstoreModel.key(123); - sinon.stub(ds, 'get').resolves([entity]); - }); + describe('get()', () => { + let entity; - afterEach(() => { - ds.get.restore(); - }); + beforeEach(() => { + entity = { name: 'John' }; + entity[ds.KEY] = GstoreModel.key(123); + sinon.stub(ds, 'get').resolves([entity]); + }); - it('passing an integer id', () => { - return GstoreModel.get(123).then(onEntity); + afterEach(() => { + ds.get.restore(); + }); - function onEntity(_entity) { - expect(ds.get.getCall(0).args[0][0].constructor.name).equal('Key'); - expect(_entity instanceof Entity).equal(true); - } - }); + it('passing an integer id', () => { + return GstoreModel.get(123).then(onEntity); - it('passing an string id', () => GstoreModel.get('keyname').then(_entity => { - expect(_entity instanceof Entity).equal(true); - })); + function onEntity(_entity) { + expect(ds.get.getCall(0).args[0][0].constructor.name).equal('Key'); + expect(_entity instanceof Entity.default).equal(true); + } + }); - it('passing an array of ids', () => { - ds.get.restore(); + it('passing an string id', () => + GstoreModel.get('keyname').then(_entity => { + expect(_entity instanceof Entity.default).equal(true); + })); - const entity1 = { name: 'John' }; - entity1[ds.KEY] = ds.key(['BlogPost', 22]); + it('passing an array of ids', () => { + ds.get.restore(); - const entity2 = { name: 'John' }; - entity2[ds.KEY] = ds.key(['BlogPost', 69]); + const entity1 = { name: 'John' }; + entity1[ds.KEY] = ds.key(['BlogPost', 22]); - sinon.stub(ds, 'get').resolves([[entity2, entity1]]); // not sorted + const entity2 = { name: 'John' }; + entity2[ds.KEY] = ds.key(['BlogPost', 69]); - return GstoreModel - .get([22, 69], null, null, null, { preserveOrder: true }) - .then(_entity => { - expect(is.array(ds.get.getCall(0).args[0])).equal(true); - expect(is.array(_entity)).equal(true); - expect(_entity[0].entityKey.id).equal(22); // sorted - }); - }); + sinon.stub(ds, 'get').resolves([[entity2, entity1]]); // not sorted - it('should consistently return an array when providing id as an Array', () => GstoreModel - .get(['abc']) - .then(_entity => { - assert.isTrue(is.array(_entity)); - })); + return GstoreModel.get([22, 69], null, null, null, { preserveOrder: true }).then(_entity => { + expect(is.array(ds.get.getCall(0).args[0])).equal(true); + expect(is.array(_entity)).equal(true); + expect(_entity[0].entityKey.id).equal(22); // sorted + }); + }); - it('not converting string with mix of number and non number', () => GstoreModel.get('123:456').then(() => { - expect(ds.get.getCall(0).args[0][0].name).equal('123:456'); - })); + it('should consistently return an array when providing id as an Array', () => + GstoreModel.get(['abc']).then(_entity => { + assert.isTrue(is.array(_entity)); + })); - it('passing an ancestor path array', () => { - const ancestors = ['Parent', 'keyname']; + it('not converting string with mix of number and non number', () => + GstoreModel.get('123:456').then(() => { + expect(ds.get.getCall(0).args[0][0].name).equal('123:456'); + })); - return GstoreModel.get(123, ancestors).then(() => { - expect(ds.get.getCall(0).args[0][0].constructor.name).equal('Key'); - expect(ds.get.getCall(0).args[0][0].parent.kind).equal(ancestors[0]); - expect(ds.get.getCall(0).args[0][0].parent.name).equal(ancestors[1]); - }); - }); + it('passing an ancestor path array', () => { + const ancestors = ['Parent', 'keyname']; - it('should allow a namespace', () => { - const namespace = 'com.mydomain-dev'; + return GstoreModel.get(123, ancestors).then(() => { + expect(ds.get.getCall(0).args[0][0].constructor.name).equal('Key'); + expect(ds.get.getCall(0).args[0][0].parent.kind).equal(ancestors[0]); + expect(ds.get.getCall(0).args[0][0].parent.name).equal(ancestors[1]); + }); + }); - return GstoreModel.get(123, null, namespace).then(() => { - expect(ds.get.getCall(0).args[0][0].namespace).equal(namespace); - }); - }); + it('should allow a namespace', () => { + const namespace = 'com.mydomain-dev'; - it('on datastore get error, should reject error', done => { - ds.get.restore(); - const error = { code: 500, message: 'Something went really bad' }; - sinon.stub(ds, 'get').rejects(error); - - GstoreModel.get(123) - .populate('test') - .catch(err => { - expect(err).equal(error); - done(); - }); - }); - - it('on no entity found, should return a "ERR_ENTITY_NOT_FOUND" error', () => { - ds.get.restore(); + return GstoreModel.get(123, null, namespace).then(() => { + expect(ds.get.getCall(0).args[0][0].namespace).equal(namespace); + }); + }); - sinon.stub(ds, 'get').resolves([]); + it('on datastore get error, should reject error', done => { + ds.get.restore(); + const error = { code: 500, message: 'Something went really bad' }; + sinon.stub(ds, 'get').rejects(error); - return GstoreModel.get(123).catch(err => { - expect(err.code).equal(gstoreErrors.errorCodes.ERR_ENTITY_NOT_FOUND); - }); + GstoreModel.get(123) + .populate('test') + .catch(err => { + expect(err).equal(error); + done(); }); + }); - it('on no entity found, should return a null', () => { - ds.get.restore(); - gstore.config.errorOnEntityNotFound = false; - sinon.stub(ds, 'get').resolves([]); + it('on no entity found, should return a "ERR_ENTITY_NOT_FOUND" error', () => { + ds.get.restore(); - return GstoreModel.get(123).then(e => { - expect(e).equal(null); - }); - }); + sinon.stub(ds, 'get').resolves([]); - it('should get in a transaction', () => GstoreModel.get(123, null, null, transaction).then(_entity => { - expect(transaction.get.called).equal(true); - expect(ds.get.called).equal(false); - expect(_entity.className).equal('Entity'); - })); + return GstoreModel.get(123).catch(err => { + expect(err.code).equal(ERROR_CODES.ERR_ENTITY_NOT_FOUND); + }); + }); - it( - 'should throw error if transaction not an instance of glcoud Transaction', - () => GstoreModel.get(123, null, null, {}).catch(err => { - expect(err.message).equal('Transaction needs to be a gcloud Transaction'); - }) - ); + it('on no entity found, should return a null', () => { + ds.get.restore(); + gstore.config.errorOnEntityNotFound = false; + sinon.stub(ds, 'get').resolves([]); - it('should return error from Transaction.get()', () => { - transaction.get.restore(); - const error = { code: 500, message: 'Houston we really need you' }; - sinon.stub(transaction, 'get').rejects(error); + return GstoreModel.get(123).then(e => { + expect(e).equal(null); + }); + }); - return GstoreModel.get(123, null, null, transaction).catch(err => { - expect(err).equal(error); - }); - }); + it('should get in a transaction', () => + GstoreModel.get(123, null, null, transaction).then(_entity => { + expect(transaction.get.called).equal(true); + expect(ds.get.called).equal(false); + expect(_entity.__className).equal('Entity'); + })); + + it('should throw error if transaction not an instance of glcoud Transaction', () => + GstoreModel.get(123, null, null, {}).catch(err => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); + + it('should return error from Transaction.get()', () => { + transaction.get.restore(); + const error = { code: 500, message: 'Houston we really need you' }; + sinon.stub(transaction, 'get').rejects(error); + + return GstoreModel.get(123, null, null, transaction).catch(err => { + expect(err).equal(error); + }); + }); - it('should still work with a callback', () => { - return GstoreModel.get(123, onResult); + it('should still work with a callback', () => { + return GstoreModel.get(123, onResult); - function onResult(err, _entity) { - expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); - expect(_entity instanceof Entity).equal(true); - } - }); + function onResult(err, _entity) { + expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); + expect(_entity instanceof Entity).equal(true); + } + }); - it('should get data through a Dataloader instance (singe key)', () => { - const dataloader = createDataLoader(ds); - const spy = sinon.stub(dataloader, 'load').resolves(entity); + it('should get data through a Dataloader instance (singe key)', () => { + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.stub(dataloader, 'load').resolves(entity); - return GstoreModel.get(123, null, null, null, { dataloader }).then(res => { - expect(spy.called).equal(true); + return GstoreModel.get(123, null, null, null, { dataloader }).then(res => { + expect(spy.called).equal(true); - const args = spy.getCall(0).args[0]; - const key = ds.key({ path: ['Blog', 123], namespace: 'com.mydomain' }); - expect(args).deep.equal(key); - expect(res.name).equal('John'); - }); - }); + const args = spy.getCall(0).args[0]; + const key = ds.key({ path: ['Blog', 123], namespace: 'com.mydomain' }); + expect(args).deep.equal(key); + expect(res.name).equal('John'); + }); + }); - it('should get data through a Dataloader instance (multiple key)', () => { - const dataloader = createDataLoader(ds); - const spy = sinon.stub(dataloader, 'loadMany').resolves([{}, {}]); + it('should get data through a Dataloader instance (multiple key)', () => { + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.stub(dataloader, 'loadMany').resolves([{}, {}]); - return GstoreModel.get([123, 456], null, null, null, { dataloader }).then(() => { - expect(spy.called).equal(true); + return GstoreModel.get([123, 456], null, null, null, { dataloader }).then(() => { + expect(spy.called).equal(true); - const args = spy.getCall(0).args[0]; - const key1 = ds.key({ path: ['Blog', 123], namespace: 'com.mydomain' }); - const key2 = ds.key({ path: ['Blog', 456], namespace: 'com.mydomain' }); + const args = spy.getCall(0).args[0]; + const key1 = ds.key({ path: ['Blog', 123], namespace: 'com.mydomain' }); + const key2 = ds.key({ path: ['Blog', 456], namespace: 'com.mydomain' }); - expect(args[0]).deep.equal(key1); - expect(args[1]).deep.equal(key2); - }); - }); + expect(args[0]).deep.equal(key1); + expect(args[1]).deep.equal(key2); + }); + }); - it('should throw an error if dataloader is not a DataLoader instance', done => { - const dataloader = {}; + it('should throw an error if dataloader is not a DataLoader instance', done => { + const dataloader = {}; + + GstoreModel.get([123, 456], null, null, null, { dataloader }).then( + () => {}, + err => { + expect(err.name).equal('GstoreError'); + expect(err.message).equal('dataloader must be a "DataLoader" instance'); + done(); + }, + ); + }); - GstoreModel.get([123, 456], null, null, null, { dataloader }).then(() => { }, err => { - expect(err.name).equal('GstoreError'); - expect(err.message).equal('dataloader must be a "DataLoader" instance'); - done(); - }); - }); + it('should allow to chain populate() calls and then call the Model.populate() method', () => { + const populateSpy = sinon.spy(GstoreModel, '__populate'); + const options = { dataLoader: { foo: 'bar' } }; - it('should allow to chain populate() calls and then call the Model.populate() method', () => { - const populateSpy = sinon.spy(GstoreModel, 'populate'); - const options = { dataLoader: { foo: 'bar' } }; - - return GstoreModel - .get(123, null, null, null, options) - .populate('company', ['name', 'phone-number']) - .then(() => { - expect(populateSpy.called).equal(true); - const { args } = populateSpy.getCall(0); - expect(args[0][0]).deep.equal([{ path: 'company', select: ['name', 'phone-number'] }]); - expect(args[1]).deep.equal({ ...options, transaction: null }); - - GstoreModel.populate.restore(); - }); - }); + return GstoreModel.get(123, null, null, null, options) + .populate('company', ['name', 'phone-number']) + .then(() => { + expect(populateSpy.called).equal(true); + const { args } = populateSpy.getCall(0); + expect(args[0][0]).deep.equal([{ path: 'company', select: ['name', 'phone-number'] }]); + expect(args[1]).deep.equal({ ...options, transaction: null }); - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); - - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should get value from cache', () => { - sinon.spy(GstoreModel.gstore.cache.keys, 'read'); - const key = GstoreModel.key(123); - const value = { name: 'Michael' }; - - return gstore.cache.keys.set(key, value) - .then(() => ( - GstoreModel.get(123, null, null, null, { ttl: 334455 }) - .then(response => { - assert.ok(!ds.get.called); - expect(response.entityData).include(value); - assert.ok(GstoreModel.gstore.cache.keys.read.called); - const { args } = GstoreModel.gstore.cache.keys.read.getCall(0); - expect(args[0].id).equal(123); - expect(args[1].ttl).equal(334455); - GstoreModel.gstore.cache.keys.read.restore(); - }) - )); - }); - - it('should throw an Error if entity not found in cache', done => { - ds.get.resolves([]); - GstoreModel.get(12345, null, null, null, { ttl: 334455 }) - .catch(err => { - expect(err.code).equal(gstoreErrors.errorCodes.ERR_ENTITY_NOT_FOUND); - done(); - }); - }); - - it('should return null if entity not found in cache', done => { - ds.get.resolves([]); - - gstore.config.errorOnEntityNotFound = false; - - GstoreModel.get(12345, null, null, null, { ttl: 334455 }) - .then(en => { - expect(en).equal(null); - gstore.config.errorOnEntityNotFound = true; - done(); - }); - }); - - it('should *not* get value from cache when deactivated in options', () => { - const key = GstoreModel.key(123); - const value = { name: 'Michael' }; - - return gstore.cache.keys.set(key, value) - .then(() => ( - GstoreModel.get(123, null, null, null, { cache: false }) - .then(response => { - assert.ok(ds.get.called); - expect(response.entityData).contains(entity); - ds.get.reset(); - ds.get.resolves([entity]); - }) - )) - .then(() => ( - GstoreModel.get(123) - .then(() => { - // Make sure we get from the cache - // if no options config is passed - assert.ok(!ds.get.called); - }) - )); - }); - - it('should *not* get value from cache when global ttl === -1', () => { - const originalConf = gstore.cache.config.ttl; - gstore.cache.config.ttl = { ...gstore.cache.config.ttl, keys: -1 }; - const key = GstoreModel.key(123); - - return gstore.cache.keys.set(key, {}) - .then(() => ( - GstoreModel.get(123) - .then(() => { - assert.ok(ds.get.called); - gstore.cache.config.ttl = originalConf; - }) - )); - }); - - it('should get value from fetchHandler', () => ( - GstoreModel.get(123) - .then(response => { - assert.ok(ds.get.called); - const { args } = ds.get.getCall(0); - expect(args[0][0].id).equal(123); - expect(response.entityData).include(entity); - }) - )); - - it('should get key from fetchHandler and Dataloader', () => { - const dataloader = createDataLoader(ds); - const spy = sinon.stub(dataloader, 'load').resolves(entity); - - return GstoreModel.get(123, null, null, null, { dataloader }).then(res => { - expect(spy.called).equal(true); - expect(res.name).equal('John'); - }); - }); - - it('should get multiple keys from fetchHandler and Dataloader', () => { - const entity2 = { name: 'Mick' }; - entity2[ds.KEY] = GstoreModel.key(456); - const dataloader = createDataLoader(ds); - const spy = sinon.stub(dataloader, 'loadMany').resolves([entity, entity2]); - - return GstoreModel.get([123, 456], null, null, null, { dataloader }).then(res => { - expect(spy.called).equal(true); - expect(res[0].name).equal('John'); - expect(res[1].name).equal('Mick'); - }); - }); - - it('should get value from cache and call the fetchHandler **only** with keys not in the cache', () => { - const key = GstoreModel.key(456); - const cacheEntity = { name: 'John' }; - cacheEntity[ds.KEY] = key; - - return gstore.cache.keys.set(key, cacheEntity) - .then(() => ( - GstoreModel.get([123, 456]) - .then(response => { - assert.ok(ds.get.called); - const { args } = ds.get.getCall(0); - expect(args[0][0].id).equal(123); - expect(response.length).equal(2); - }) - )); - }); - - it('should allow to chain populate() calls and then call the Model.populate() method', () => { - const spy = sinon.spy(GstoreModel, 'populate'); - - const key = GstoreModel.key(123); - const value = { foo: 'bar' }; - - return gstore.cache.keys.set(key, value) - .then(() => ( - GstoreModel.get(123) - .populate('company', ['name', 'phone-number']) - .then(() => { - expect(spy.called).equal(true); - const { args } = spy.getCall(0); - expect(args[0][0]).deep.equal([ - { path: 'company', select: ['name', 'phone-number'] }, - ]); - }) - )); - }); + GstoreModel.__populate.restore(); }); }); - describe('update()', () => { - it('should run in a transaction', () => GstoreModel.update(123).then(() => { - expect(ds.transaction.called).equal(true); - expect(transaction.run.called).equal(true); - expect(transaction.commit.called).equal(true); - })); - - it('should return an entity instance', () => GstoreModel.update(123).then(entity => { - expect(entity.className).equal('Entity'); + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should get value from cache', () => { + sinon.spy(GstoreModel.gstore.cache.keys, 'read'); + const key = GstoreModel.key(123); + const value = { name: 'Michael' }; + + return gstore.cache.keys.set(key, value).then(() => + GstoreModel.get(123, null, null, null, { ttl: 334455 }).then(response => { + assert.ok(!ds.get.called); + expect(response.entityData).include(value); + assert.ok(GstoreModel.gstore.cache.keys.read.called); + const { args } = GstoreModel.gstore.cache.keys.read.getCall(0); + expect(args[0].id).equal(123); + expect(args[1].ttl).equal(334455); + GstoreModel.gstore.cache.keys.read.restore(); + }), + ); + }); + + it('should throw an Error if entity not found in cache', done => { + ds.get.resolves([]); + GstoreModel.get(12345, null, null, null, { ttl: 334455 }).catch(err => { + expect(err.code).equal(ERROR_CODES.ERR_ENTITY_NOT_FOUND); + done(); + }); + }); + + it('should return null if entity not found in cache', done => { + ds.get.resolves([]); + + gstore.config.errorOnEntityNotFound = false; + + GstoreModel.get(12345, null, null, null, { ttl: 334455 }).then(en => { + expect(en).equal(null); + gstore.config.errorOnEntityNotFound = true; + done(); + }); + }); + + it('should *not* get value from cache when deactivated in options', () => { + const key = GstoreModel.key(123); + const value = { name: 'Michael' }; + + return gstore.cache.keys + .set(key, value) + .then(() => + GstoreModel.get(123, null, null, null, { cache: false }).then(response => { + assert.ok(ds.get.called); + expect(response.entityData).contains(entity); + ds.get.reset(); + ds.get.resolves([entity]); + }), + ) + .then(() => + GstoreModel.get(123).then(() => { + // Make sure we get from the cache + // if no options config is passed + assert.ok(!ds.get.called); + }), + ); + }); + + it('should *not* get value from cache when global ttl === -1', () => { + const originalConf = gstore.cache.config.ttl; + gstore.cache.config.ttl = { ...gstore.cache.config.ttl, keys: -1 }; + const key = GstoreModel.key(123); + + return gstore.cache.keys.set(key, {}).then(() => + GstoreModel.get(123).then(() => { + assert.ok(ds.get.called); + gstore.cache.config.ttl = originalConf; + }), + ); + }); + + it('should get value from fetchHandler', () => + GstoreModel.get(123).then(response => { + assert.ok(ds.get.called); + const { args } = ds.get.getCall(0); + expect(args[0][0].id).equal(123); + expect(response.entityData).include(entity); })); - it('should first get the entity by Key', () => ( - GstoreModel.update(123).then(() => { - expect(transaction.get.getCall(0).args[0].constructor.name).equal('Key'); - expect(transaction.get.getCall(0).args[0].path[1]).equal(123); - }) - )); - - it('should not convert a string id with mix of number and alpha chars', () => ( - GstoreModel.update('123:456').then(() => { - expect(transaction.get.getCall(0).args[0].name).equal('123:456'); - }) - )); - - it('should rollback if error while getting entity', () => { - transaction.get.restore(); - const error = { code: 500, message: 'Houston we got a problem' }; - sinon.stub(transaction, 'get').rejects(error); - - return GstoreModel.update(123).catch(err => { - expect(err).deep.equal(error); - expect(transaction.rollback.called).equal(true); - expect(transaction.commit.called).equal(false); - }); - }); + it('should get key from fetchHandler and Dataloader', () => { + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.stub(dataloader, 'load').resolves(entity); + + return GstoreModel.get(123, null, null, null, { dataloader }).then(res => { + expect(spy.called).equal(true); + expect(res.name).equal('John'); + }); + }); + + it('should get multiple keys from fetchHandler and Dataloader', () => { + const entity2 = { name: 'Mick' }; + entity2[ds.KEY] = GstoreModel.key(456); + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.stub(dataloader, 'loadMany').resolves([entity, entity2]); + + return GstoreModel.get([123, 456], null, null, null, { dataloader }).then(res => { + expect(spy.called).equal(true); + expect(res[0].name).equal('John'); + expect(res[1].name).equal('Mick'); + }); + }); + + it('should get value from cache and call the fetchHandler **only** with keys not in the cache', () => { + const key = GstoreModel.key(456); + const cacheEntity = { name: 'John' }; + cacheEntity[ds.KEY] = key; + + return gstore.cache.keys.set(key, cacheEntity).then(() => + GstoreModel.get([123, 456]).then(response => { + assert.ok(ds.get.called); + const { args } = ds.get.getCall(0); + expect(args[0][0].id).equal(123); + expect(response.length).equal(2); + }), + ); + }); - it('should return "ERR_ENTITY_NOT_FOUND" if entity not found', () => { - transaction.get.restore(); - sinon.stub(transaction, 'get').resolves([]); + it('should allow to chain populate() calls and then call the Model.populate() method', () => { + const spy = sinon.spy(GstoreModel, '__populate'); - return GstoreModel.update('keyname').catch(err => { - expect(err.code).equal(gstoreErrors.errorCodes.ERR_ENTITY_NOT_FOUND); - }); - }); + const key = GstoreModel.key(123); + const value = { foo: 'bar' }; - it('should return error if any while saving', done => { - transaction.run.restore(); - const error = { code: 500, message: 'Houston wee need you.' }; - sinon.stub(transaction, 'run').rejects([error]); + return gstore.cache.keys.set(key, value).then(() => + GstoreModel.get(123) + .populate('company', ['name', 'phone-number']) + .then(() => { + expect(spy.called).equal(true); + const { args } = spy.getCall(0); + expect(args[0][0]).deep.equal([{ path: 'company', select: ['name', 'phone-number'] }]); - GstoreModel.update(123).catch(err => { - expect(err).equal(error); - done(); - }); - }); + GstoreModel.__populate.restore(); + }), + ); + }); + }); + }); + + describe('update()', () => { + it('should run in a transaction', () => + GstoreModel.update(123).then(() => { + expect(ds.transaction.called).equal(true); + expect(transaction.run.called).equal(true); + expect(transaction.commit.called).equal(true); + })); + + it('should return an entity instance', () => + GstoreModel.update(123).then(entity => { + expect(entity.__className).equal('Entity'); + })); + + it('should first get the entity by Key', () => + GstoreModel.update(123).then(() => { + expect(transaction.get.getCall(0).args[0].constructor.name).equal('Key'); + expect(transaction.get.getCall(0).args[0].path[1]).equal(123); + })); + + it('should not convert a string id with mix of number and alpha chars', () => + GstoreModel.update('123:456').then(() => { + expect(transaction.get.getCall(0).args[0].name).equal('123:456'); + })); + + it('should rollback if error while getting entity', () => { + transaction.get.restore(); + const error = { code: 500, message: 'Houston we got a problem' }; + sinon.stub(transaction, 'get').rejects(error); + + return GstoreModel.update(123).catch(err => { + expect(err).deep.equal(error); + expect(transaction.rollback.called).equal(true); + expect(transaction.commit.called).equal(false); + }); + }); - it('accept an ancestor path', () => { - const ancestors = ['Parent', 'keyname']; + it('should return "ERR_ENTITY_NOT_FOUND" if entity not found', () => { + transaction.get.restore(); + sinon.stub(transaction, 'get').resolves([]); - return GstoreModel.update(123, {}, ancestors).then(() => { - expect(transaction.get.getCall(0).args[0].path[0]).equal('Parent'); - expect(transaction.get.getCall(0).args[0].path[1]).equal('keyname'); - }); - }); + return GstoreModel.update('keyname').catch(err => { + expect(err.code).equal(ERROR_CODES.ERR_ENTITY_NOT_FOUND); + }); + }); - it('should allow a namespace', () => { - const namespace = 'com.mydomain-dev'; + it('should return error if any while saving', done => { + transaction.run.restore(); + const error = { code: 500, message: 'Houston wee need you.' }; + sinon.stub(transaction, 'run').rejects([error]); - return GstoreModel.update(123, {}, null, namespace).then(() => { - expect(transaction.get.getCall(0).args[0].namespace).equal(namespace); - }); - }); + GstoreModel.update(123).catch(err => { + expect(err).equal(error); + done(); + }); + }); - it('should save and replace data', () => { - const data = { name: 'Mick' }; - return GstoreModel.update(123, data, null, null, null, { replace: true }) - .then(entity => { - expect(entity.entityData.name).equal('Mick'); - expect(entity.entityData.lastname).equal(null); - expect(entity.entityData.email).equal(null); - }); - }); + it('accept an ancestor path', () => { + const ancestors = ['Parent', 'keyname']; - it('should accept a DataLoader instance, add it to the entity created and clear the key', () => { - const dataloader = createDataLoader(ds); - const spy = sinon.spy(dataloader, 'clear'); - - return GstoreModel.update(123, {}, null, null, null, { dataloader }) - .then(entity => { - const keyToClear = spy.getCalls()[0].args[0]; - expect(keyToClear.kind).equal('Blog'); - expect(keyToClear.id).equal(123); - expect(entity.dataloader).equal(dataloader); - }); - }); + return GstoreModel.update(123, {}, ancestors).then(() => { + expect(transaction.get.getCall(0).args[0].path[0]).equal('Parent'); + expect(transaction.get.getCall(0).args[0].path[1]).equal('keyname'); + }); + }); - it('should merge the new data with the entity data', () => { - const data = { - name: 'Sebas', - lastname: 'Snow', - }; - return GstoreModel.update(123, data, ['Parent', 'keyNameParent']) - .then(entity => { - expect(entity.entityData.name).equal('Sebas'); - expect(entity.entityData.lastname).equal('Snow'); - expect(entity.entityData.email).equal('john@snow.com'); - }); - }); + it('should allow a namespace', () => { + const namespace = 'com.mydomain-dev'; - it('should call save() on the transaction', () => { - GstoreModel.update(123, {}).then(() => { - expect(transaction.save.called).equal(true); - }); - }); + return GstoreModel.update(123, {}, null, namespace).then(() => { + expect(transaction.get.getCall(0).args[0].namespace).equal(namespace); + }); + }); - it('should return error and rollback transaction if not passing validation', () => ( - GstoreModel.update(123, { unknown: 1 }) - .catch(err => { - assert.isDefined(err); - expect(transaction.rollback.called).equal(true); - }) - )); + it('should save and replace data', () => { + const data = { name: 'Mick' }; + return GstoreModel.update(123, data, null, null, null, { replace: true }).then(entity => { + expect(entity.entityData.name).equal('Mick'); + expect(entity.entityData.lastname).equal(null); + expect(entity.entityData.email).equal(null); + }); + }); - it('should return error if not passing validation', () => ( - GstoreModel.update(123, { unknown: 1 }, null, null, null, { replace: true }) - .catch(err => { - assert.isDefined(err); - }) - )); + it('should accept a DataLoader instance, add it to the entity created and clear the key', () => { + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.spy(dataloader, 'clear'); - it('should run inside an *existing* transaction', () => ( - GstoreModel.update(123, {}, null, null, transaction) - .then(entity => { - expect(ds.transaction.called).equal(false); - expect(transaction.get.called).equal(true); - expect(transaction.save.called).equal(true); - expect(entity.className).equal('Entity'); - }) - )); + return GstoreModel.update(123, {}, null, null, null, { dataloader }).then(entity => { + const keyToClear = spy.getCalls()[0].args[0]; + expect(keyToClear.kind).equal('Blog'); + expect(keyToClear.id).equal(123); + expect(entity.dataloader).equal(dataloader); + }); + }); - it('should throw error if transaction passed is not instance of gcloud Transaction', () => ( - GstoreModel.update(123, {}, null, null, {}) - .catch(err => { - expect(err.message).equal('Transaction needs to be a gcloud Transaction'); - }) - )); + it('should merge the new data with the entity data', () => { + const data = { + name: 'Sebas', + lastname: 'Snow', + }; + return GstoreModel.update(123, data, ['Parent', 'keyNameParent']).then(entity => { + expect(entity.entityData.name).equal('Sebas'); + expect(entity.entityData.lastname).equal('Snow'); + expect(entity.entityData.email).equal('john@snow.com'); + }); + }); - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); - - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should call Model.clearCache() passing the key updated', () => { - sinon.spy(GstoreModel, 'clearCache'); - return GstoreModel.update(123, { name: 'Nuri' }, ['Parent', 'keyNameParent']) - .then(entity => { - assert.ok(GstoreModel.clearCache.called); - expect(GstoreModel.clearCache.getCall(0).args[0].id).equal(123); - expect(entity.name).equal('Nuri'); - GstoreModel.clearCache.restore(); - }); - }); - - it('on error when clearing the cache, should add the entityUpdated on the error', done => { - const err = new Error('Houston something bad happened'); - sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - - GstoreModel.update(123, { name: 'Nuri' }) - .catch(e => { - expect(e.__entityUpdated.name).equal('Nuri'); - expect(e.__cacheError).equal(err); - gstore.cache.queries.clearQueriesByKind.restore(); - done(); - }); - }); - }); + it('should call save() on the transaction', () => { + GstoreModel.update(123, {}).then(() => { + expect(transaction.save.called).equal(true); + }); }); - describe('delete()', () => { - beforeEach(() => { - sinon.stub(ds, 'delete').resolves([{ indexUpdates: 3 }]); - sinon.stub(transaction, 'delete').callsFake(() => true); - }); + it('should return error and rollback transaction if not passing validation', () => + GstoreModel.update(123, { unknown: 1 }).catch(err => { + assert.isDefined(err); + expect(transaction.rollback.called).equal(true); + })); + + it('should return error if not passing validation', () => + GstoreModel.update(123, { unknown: 1 }, null, null, null, { replace: true }).catch(err => { + assert.isDefined(err); + })); + + it('should run inside an *existing* transaction', () => + GstoreModel.update(123, {}, null, null, transaction).then(entity => { + expect(ds.transaction.called).equal(false); + expect(transaction.get.called).equal(true); + expect(transaction.save.called).equal(true); + expect(entity.__className).equal('Entity'); + })); + + it('should throw error if transaction passed is not instance of gcloud Transaction', () => + GstoreModel.update(123, {}, null, null, {}).catch(err => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); + + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should call Model.clearCache() passing the key updated', () => { + sinon.spy(GstoreModel, 'clearCache'); + return GstoreModel.update(123, { name: 'Nuri' }, ['Parent', 'keyNameParent']).then(entity => { + assert.ok(GstoreModel.clearCache.called); + expect(GstoreModel.clearCache.getCall(0).args[0].id).equal(123); + expect(entity.name).equal('Nuri'); + GstoreModel.clearCache.restore(); + }); + }); + + it('on error when clearing the cache, should add the entityUpdated on the error', done => { + const err = new Error('Houston something bad happened'); + sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); + + GstoreModel.update(123, { name: 'Nuri' }).catch(e => { + expect(e.__entityUpdated.name).equal('Nuri'); + expect(e.__cacheError).equal(err); + gstore.cache.queries.clearQueriesByKind.restore(); + done(); + }); + }); + }); + }); - afterEach(() => { - ds.delete.restore(); - transaction.delete.restore(); - }); + describe('delete()', () => { + beforeEach(() => { + sinon.stub(ds, 'delete').resolves([{ indexUpdates: 3 }]); + sinon.stub(transaction, 'delete').callsFake(() => true); + }); - it('should call ds.delete with correct Key (int id)', () => ( - GstoreModel.delete(123).then(response => { - expect(ds.delete.called).equal(true); - expect(ds.delete.getCall(0).args[0].constructor.name).equal('Key'); - expect(response.success).equal(true); - }) - )); + afterEach(() => { + ds.delete.restore(); + transaction.delete.restore(); + }); - it('should call ds.delete with correct Key (string id)', () => ( - GstoreModel.delete('keyName') - .then(response => { - expect(ds.delete.called).equal(true); - expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); - expect(response.success).equal(true); - }) - )); + it('should call ds.delete with correct Key (int id)', () => + GstoreModel.delete(123).then(response => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].constructor.name).equal('Key'); + expect(response.success).equal(true); + })); + + it('should call ds.delete with correct Key (string id)', () => + GstoreModel.delete('keyName').then(response => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); + expect(response.success).equal(true); + })); + + it('not converting string id with mix of number and alpha chars', () => + GstoreModel.delete('123:456').then(() => { + expect(ds.delete.getCall(0).args[0].name).equal('123:456'); + })); + + it('should allow array of ids', () => + GstoreModel.delete([22, 69]).then(() => { + expect(is.array(ds.delete.getCall(0).args[0])).equal(true); + })); + + it('should allow ancestors', () => + GstoreModel.delete(123, ['Parent', 123]).then(() => { + const key = ds.delete.getCall(0).args[0]; + + expect(key.parent.kind).equal('Parent'); + expect(key.parent.id).equal(123); + })); + + it('should allow a namespace', () => { + const namespace = 'com.mydomain-dev'; + + return GstoreModel.delete('keyName', null, namespace).then(() => { + const key = ds.delete.getCall(0).args[0]; + + expect(key.namespace).equal(namespace); + }); + }); - it('not converting string id with mix of number and alpha chars', () => ( - GstoreModel.delete('123:456') - .then(() => { - expect(ds.delete.getCall(0).args[0].name).equal('123:456'); - }) - )); + it('should delete entity in a transaction', () => + GstoreModel.delete(123, null, null, transaction).then(() => { + expect(transaction.delete.called).equal(true); + expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); + })); + + it('should deal with empty responses', () => { + ds.delete.restore(); + sinon.stub(ds, 'delete').resolves(); + return GstoreModel.delete(1).then(response => { + assert.isDefined(response.key); + }); + }); - it('should allow array of ids', () => GstoreModel.delete([22, 69]).then(() => { - expect(is.array(ds.delete.getCall(0).args[0])).equal(true); - })); + it('should delete entity in a transaction in sync', () => { + GstoreModel.delete(123, null, null, transaction); + expect(transaction.delete.called).equal(true); + expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); + }); - it('should allow ancestors', () => GstoreModel.delete(123, ['Parent', 123]).then(() => { - const key = ds.delete.getCall(0).args[0]; + it('should throw error if transaction passed is not instance of gcloud Transaction', () => + GstoreModel.delete(123, null, null, {}).catch(err => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); - expect(key.parent.kind).equal('Parent'); - expect(key.parent.id).equal(123); - })); + it('should set "success" to false if no entity deleted', () => { + ds.delete.restore(); + sinon.stub(ds, 'delete').resolves([{ indexUpdates: 0 }]); - it('should allow a namespace', () => { - const namespace = 'com.mydomain-dev'; + return GstoreModel.delete(123).then(response => { + expect(response.success).equal(false); + }); + }); - return GstoreModel.delete('keyName', null, namespace).then(() => { - const key = ds.delete.getCall(0).args[0]; + it('should not set success neither apiRes', () => { + ds.delete.restore(); + sinon.stub(ds, 'delete').resolves([{}]); - expect(key.namespace).equal(namespace); - }); - }); + return GstoreModel.delete(123).then(response => { + assert.isUndefined(response.success); + }); + }); - it('should delete entity in a transaction', () => ( - GstoreModel.delete(123, null, null, transaction) - .then(() => { - expect(transaction.delete.called).equal(true); - expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); - }) - )); + it('should handle errors', () => { + ds.delete.restore(); + const error = { code: 500, message: 'We got a problem Houston' }; + sinon.stub(ds, 'delete').rejects(error); - it('should deal with empty responses', () => { - ds.delete.restore(); - sinon.stub(ds, 'delete').resolves(); - return GstoreModel.delete(1).then(response => { - assert.isDefined(response.key); - }); - }); + return GstoreModel.delete(123).catch(err => { + expect(err).equal(error); + }); + }); - it('should delete entity in a transaction in sync', () => { - GstoreModel.delete(123, null, null, transaction); - expect(transaction.delete.called).equal(true); - expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); - }); + it('should call pre hooks', () => { + const spy = { + beforeSave: () => Promise.resolve(), + }; + sinon.spy(spy, 'beforeSave'); + schema.pre('delete', spy.beforeSave); + GstoreModel = gstore.model('Blog-1', schema); + + return GstoreModel.delete(123).then(() => { + expect(spy.beforeSave.calledBefore(ds.delete)).equal(true); + }); + }); - it('should throw error if transaction passed is not instance of gcloud Transaction', () => ( - GstoreModel.delete(123, null, null, {}) - .catch(err => { - expect(err.message).equal('Transaction needs to be a gcloud Transaction'); - }) - )); + it('pre hook should override id passed', () => { + const spy = { + beforeSave: () => Promise.resolve({ __override: [666] }), + }; + sinon.spy(spy, 'beforeSave'); + schema.pre('delete', spy.beforeSave); + GstoreModel = gstore.model('Blog-2', schema); + + return GstoreModel.delete(123).then(() => { + expect(ds.delete.getCall(0).args[0].id).equal(666); + }); + }); - it('should set "success" to false if no entity deleted', () => { - ds.delete.restore(); - sinon.stub(ds, 'delete').resolves([{ indexUpdates: 0 }]); + it('should set "pre" hook scope to entity being deleted (1)', done => { + schema.pre('delete', function preDelete() { + expect(this instanceof Entity.default).equal(true); + done(); + return Promise.resolve(); + }); + GstoreModel = gstore.model('Blog-3', schema); - return GstoreModel.delete(123).then(response => { - expect(response.success).equal(false); - }); - }); + GstoreModel.delete(123); + }); - it('should not set success neither apiRes', () => { - ds.delete.restore(); - sinon.stub(ds, 'delete').resolves([{}]); + it('should set "pre" hook scope to entity being deleted (2)', () => { + schema.pre('delete', function preDelete() { + expect(this.entityKey.id).equal(777); + return Promise.resolve(); + }); + GstoreModel = gstore.model('Blog-4', schema); - return GstoreModel.delete(123).then(response => { - assert.isUndefined(response.success); - }); - }); + // ... passing a datastore.key + return GstoreModel.delete(null, null, null, null, GstoreModel.key(777)); + }); - it('should handle errors', () => { - ds.delete.restore(); - const error = { code: 500, message: 'We got a problem Houston' }; - sinon.stub(ds, 'delete').rejects(error); + it('should NOT set "pre" hook scope if deleting an array of ids', () => { + let scope; + schema.pre('delete', function preDelete() { + scope = this; + return Promise.resolve(); + }); + GstoreModel = gstore.model('Blog-5', schema); + + return GstoreModel.delete([123, 456]).then(() => { + expect(scope).equal(null); + }); + }); - return GstoreModel.delete(123).catch(err => { - expect(err).equal(error); - }); - }); + it('should call post hooks', () => { + const spy = { + afterDelete: () => Promise.resolve(), + }; + sinon.spy(spy, 'afterDelete'); + schema.post('delete', spy.afterDelete); + GstoreModel = gstore.model('Blog-6', schema); + + return GstoreModel.delete(123).then(() => { + expect(spy.afterDelete.called).equal(true); + }); + }); - it('should call pre hooks', () => { - const spy = { - beforeSave: () => Promise.resolve(), - }; - sinon.spy(spy, 'beforeSave'); - schema.pre('delete', spy.beforeSave); - GstoreModel = Model.compile('Blog', schema, gstore); - - return GstoreModel.delete(123).then(() => { - expect(spy.beforeSave.calledBefore(ds.delete)).equal(true); - }); - }); + it('should pass key deleted to post hooks and set the scope to the entity deleted', done => { + schema.post('delete', function postDeleteHook({ key }) { + expect(key.constructor.name).equal('Key'); + expect(key.id).equal(123); + expect(this instanceof Entity.default).equal(true); + expect(this.entityKey).equal(key); + done(); + return Promise.resolve(); + }); + GstoreModel = gstore.model('Blog-7', schema); + + GstoreModel.delete(123); + }); - it('pre hook should override id passed', () => { - const spy = { - beforeSave: () => Promise.resolve({ __override: [666] }), - }; - sinon.spy(spy, 'beforeSave'); - schema.pre('delete', spy.beforeSave); - GstoreModel = Model.compile('Blog', schema, gstore); - - return GstoreModel.delete(123).then(() => { - expect(ds.delete.getCall(0).args[0].id).equal(666); - }); - }); + it('should pass array of keys deleted to post hooks', () => { + const ids = [123, 456]; + schema.post('delete', response => { + expect(response.key.length).equal(ids.length); + expect(response.key[1].id).equal(456); + return Promise.resolve(); + }); + GstoreModel = gstore.model('Blog-8', schema); - it('should set "pre" hook scope to entity being deleted (1)', done => { - schema.pre('delete', function preDelete() { - expect(this instanceof Entity).equal(true); - done(); - return Promise.resolve(); - }); - GstoreModel = Model.compile('Blog', schema, gstore); + return GstoreModel.delete(ids).then(() => {}); + }); - GstoreModel.delete(123); - }); + it('transaction.execPostHooks() should call post hooks', () => { + const spy = { + afterDelete: () => Promise.resolve(), + }; + sinon.spy(spy, 'afterDelete'); + schema = new Schema({ name: { type: String } }); + schema.post('delete', spy.afterDelete); - it('should set "pre" hook scope to entity being deleted (2)', () => { - schema.pre('delete', function preDelete() { - expect(this.entityKey.id).equal(777); - return Promise.resolve(); - }); - GstoreModel = Model.compile('Blog', schema, gstore); + GstoreModel = gstore.model('Blog-9', schema); - // ... passing a datastore.key - return GstoreModel.delete(null, null, null, null, GstoreModel.key(777)); + return GstoreModel.delete(123, null, null, transaction).then(() => { + transaction.execPostHooks().then(() => { + expect(spy.afterDelete.called).equal(true); + expect(spy.afterDelete.calledOnce).equal(true); }); + }); + }); - it('should NOT set "pre" hook scope if deleting an array of ids', () => { - schema.pre('delete', function preDelete() { - expect(this).equal(null); - return Promise.resolve(); - }); - GstoreModel = Model.compile('Blog', schema, gstore); + it('should still work passing a callback', () => { + GstoreModel.delete('keyName', (err, response) => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); + expect(response.success).equal(true); + }); + }); - return GstoreModel.delete([123, 456], () => { }); - }); + it('should accept a DataLoader instance and clear the cached key after deleting', () => { + const dataloader = gstore.createDataLoader(ds); + const spy = sinon.spy(dataloader, 'clear'); - it('should call post hooks', () => { - const spy = { - afterDelete: () => Promise.resolve(), - }; - sinon.spy(spy, 'afterDelete'); - schema.post('delete', spy.afterDelete); - GstoreModel = Model.compile('Blog', schema, gstore); - - return GstoreModel.delete(123).then(() => { - expect(spy.afterDelete.called).equal(true); - }); - }); + return GstoreModel.delete(123, null, null, null, null, { dataloader }).then(() => { + const keyToClear = spy.getCalls()[0].args[0]; + expect(keyToClear.kind).equal('Blog'); + expect(keyToClear.id).equal(123); + }); + }); - it('should pass key deleted to post hooks and set the scope to the entity deleted', () => { - schema.post('delete', function postDeleteHook({ key }) { - expect(key.constructor.name).equal('Key'); - expect(key.id).equal(123); - expect(this instanceof Entity).equal(true); - expect(this.entityKey).equal(key); - return Promise.resolve(); - }); - GstoreModel = Model.compile('Blog', schema, gstore); - - return GstoreModel.delete(123).then(() => { }); - }); + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); - it('should pass array of keys deleted to post hooks', () => { - const ids = [123, 456]; - schema.post('delete', response => { - expect(response.key.length).equal(ids.length); - expect(response.key[1].id).equal(456); - return Promise.resolve(); - }); - GstoreModel = Model.compile('Blog', schema, gstore); + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); - return GstoreModel.delete(ids).then(() => { }); - }); + it('should call Model.clearCache() passing the key deleted', () => { + sinon.spy(GstoreModel, 'clearCache'); - it('transaction.execPostHooks() should call post hooks', () => { - const spy = { - afterDelete: () => Promise.resolve(), - }; - sinon.spy(spy, 'afterDelete'); - schema = new Schema({ name: { type: String } }); - schema.post('delete', spy.afterDelete); - - GstoreModel = Model.compile('Blog', schema, gstore); - - return GstoreModel.delete(123, null, null, transaction).then(() => { - transaction.execPostHooks().then(() => { - expect(spy.afterDelete.called).equal(true); - expect(spy.afterDelete.calledOnce).equal(true); - }); - }); + return GstoreModel.delete(445566).then(response => { + assert.ok(GstoreModel.clearCache.called); + expect(GstoreModel.clearCache.getCall(0).args[0].id).equal(445566); + expect(response.success).equal(true); + GstoreModel.clearCache.restore(); }); + }); - it('should still work passing a callback', () => { - GstoreModel.delete('keyName', (err, response) => { - expect(ds.delete.called).equal(true); - expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); - expect(response.success).equal(true); - }); - }); - - it('should accept a DataLoader instance and clear the cached key after deleting', () => { - const dataloader = createDataLoader(ds); - const spy = sinon.spy(dataloader, 'clear'); - - return GstoreModel.delete(123, null, null, null, null, { dataloader }) - .then(() => { - const keyToClear = spy.getCalls()[0].args[0]; - expect(keyToClear.kind).equal('Blog'); - expect(keyToClear.id).equal(123); - }); - }); + it('on error when clearing the cache, should add the entityUpdated on the error', done => { + const err = new Error('Houston something bad happened'); + sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); - - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should call Model.clearCache() passing the key deleted', () => { - sinon.spy(GstoreModel, 'clearCache'); - - return GstoreModel.delete(445566) - .then(response => { - assert.ok(GstoreModel.clearCache.called); - expect(GstoreModel.clearCache.getCall(0).args[0].id).equal(445566); - expect(response.success).equal(true); - GstoreModel.clearCache.restore(); - }); - }); - - it('on error when clearing the cache, should add the entityUpdated on the error', done => { - const err = new Error('Houston something bad happened'); - sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - - GstoreModel.delete(1234) - .catch(e => { - expect(e.__response.success).equal(true); - expect(e.__cacheError).equal(err); - gstore.cache.queries.clearQueriesByKind.restore(); - done(); - }); - }); + GstoreModel.delete(1234).catch(e => { + expect(e.__response.success).equal(true); + expect(e.__cacheError).equal(err); + gstore.cache.queries.clearQueriesByKind.restore(); + done(); }); + }); + }); + }); + + describe('deleteAll()', () => { + let queryMock; + + beforeEach(() => { + queryMock = new Query(ds, { entities: mockEntities }); + sinon.spy(queryMock, 'run'); + sinon.spy(queryMock, 'hasAncestor'); + sinon.stub(ds, 'createQuery').callsFake(() => queryMock); + + sinon.stub(ds, 'delete').callsFake(() => { + // We need to update our mock response of the Query + // to not enter in an infinite loop as we recursivly query + // until there are no more entities + ds.createQuery.restore(); + sinon.stub(ds, 'createQuery').callsFake(() => new Query(ds, { entities: [] })); + return Promise.resolve([{ indexUpdates: 3 }]); + }); + + sinon.spy(GstoreModel, 'query'); }); - describe('deleteAll()', () => { - let queryMock; - - beforeEach(() => { - queryMock = new Query(ds, { entities: mockEntities }); - sinon.spy(queryMock, 'run'); - sinon.spy(queryMock, 'hasAncestor'); - sinon.stub(ds, 'createQuery').callsFake(() => queryMock); - - sinon.stub(ds, 'delete').callsFake(() => { - // We need to update our mock response of the Query - // to not enter in an infinite loop as we recursivly query - // until there are no more entities - ds.createQuery.restore(); - sinon.stub(ds, 'createQuery').callsFake(() => new Query(ds, { entities: [] })); - return Promise.resolve([{ indexUpdates: 3 }]); - }); - - sinon.spy(GstoreModel, 'initQuery'); - }); - - afterEach(() => { - ds.delete.restore(); - ds.createQuery.restore(); - if (queryMock.run.restore) { - queryMock.run.restore(); - } - if (queryMock.hasAncestor.restore) { - queryMock.hasAncestor.restore(); - } - }); - - it('should get all entities through Query', () => ( - GstoreModel.deleteAll().then(() => { - expect(GstoreModel.initQuery.called).equal(true); - expect(GstoreModel.initQuery.getCall(0).args.length).equal(1); - }) - )); + afterEach(() => { + ds.delete.restore(); + ds.createQuery.restore(); + if (queryMock.run.restore) { + queryMock.run.restore(); + } + if (queryMock.hasAncestor.restore) { + queryMock.hasAncestor.restore(); + } + }); - it('should catch error if could not fetch entities', () => { - const error = { code: 500, message: 'Something went wrong' }; - queryMock.run.restore(); - sinon.stub(queryMock, 'run').rejects(error); + it('should get all entities through Query', () => + GstoreModel.deleteAll().then(() => { + expect(GstoreModel.query.called).equal(true); + expect(GstoreModel.query.getCall(0).args.length).equal(1); + })); - return GstoreModel.deleteAll().catch(err => { - expect(err).equal(error); - }); - }); + it('should catch error if could not fetch entities', () => { + const error = { code: 500, message: 'Something went wrong' }; + queryMock.run.restore(); + sinon.stub(queryMock, 'run').rejects(error); - it('if pre hooks, should call "delete" on all entities found (in series)', () => { - schema = new Schema({}); - const spies = { - pre: () => Promise.resolve(), - }; - sinon.spy(spies, 'pre'); + return GstoreModel.deleteAll().catch(err => { + expect(err).equal(error); + }); + }); - schema.pre('delete', spies.pre); + it('if pre hooks, should call "delete" on all entities found (in series)', () => { + schema = new Schema({}); + const spies = { + pre: () => Promise.resolve(), + }; + sinon.spy(spies, 'pre'); - GstoreModel = gstore.model('NewBlog', schema); - sinon.spy(GstoreModel, 'delete'); + schema.pre('delete', spies.pre); - return GstoreModel.deleteAll().then(() => { - expect(spies.pre.callCount).equal(mockEntities.length); - expect(GstoreModel.delete.callCount).equal(mockEntities.length); - expect(GstoreModel.delete.getCall(0).args.length).equal(5); - expect(GstoreModel.delete.getCall(0).args[4].constructor.name).equal('Key'); - }); - }); + GstoreModel = gstore.model('NewBlog', schema); + sinon.spy(GstoreModel, 'delete'); - it('if post hooks, should call "delete" on all entities found (in series)', () => { - schema = new Schema({}); - const spies = { - post: () => Promise.resolve(), - }; - sinon.spy(spies, 'post'); - schema.post('delete', spies.post); - - GstoreModel = gstore.model('NewBlog', schema); - sinon.spy(GstoreModel, 'delete'); - - return GstoreModel.deleteAll().then(() => { - expect(spies.post.callCount).equal(mockEntities.length); - expect(GstoreModel.delete.callCount).equal(2); - }); - }); + return GstoreModel.deleteAll().then(() => { + expect(spies.pre.callCount).equal(mockEntities.length); + expect(GstoreModel.delete.callCount).equal(mockEntities.length); + expect(GstoreModel.delete.getCall(0).args.length).equal(5); + expect(GstoreModel.delete.getCall(0).args[4].constructor.name).equal('Key'); + }); + }); - it('if NO hooks, should call delete passing an array of keys', () => { - sinon.spy(GstoreModel, 'delete'); + it('if post hooks, should call "delete" on all entities found (in series)', () => { + schema = new Schema({}); + const spies = { + post: () => Promise.resolve(), + }; + sinon.spy(spies, 'post'); + schema.post('delete', spies.post); + + GstoreModel = gstore.model('NewBlog', schema); + sinon.spy(GstoreModel, 'delete'); + + return GstoreModel.deleteAll().then(() => { + expect(spies.post.callCount).equal(mockEntities.length); + expect(GstoreModel.delete.callCount).equal(2); + }); + }); - return GstoreModel.deleteAll().then(() => { - expect(GstoreModel.delete.callCount).equal(1); + it('if NO hooks, should call delete passing an array of keys', () => { + sinon.spy(GstoreModel, 'delete'); - const { args } = GstoreModel.delete.getCall(0); - expect(is.array(args[4])).equal(true); - expect(args[4]).deep.equal([mockEntities[0][ds.KEY], mockEntities[1][ds.KEY]]); + return GstoreModel.deleteAll().then(() => { + expect(GstoreModel.delete.callCount).equal(1); - GstoreModel.delete.restore(); - }); - }); + const { args } = GstoreModel.delete.getCall(0); + expect(is.array(args[4])).equal(true); + expect(args[4]).deep.equal([mockEntities[0][ds.KEY], mockEntities[1][ds.KEY]]); - it('should call with ancestors', () => { - const ancestors = ['Parent', 'keyname']; + GstoreModel.delete.restore(); + }); + }); - return GstoreModel.deleteAll(ancestors).then(() => { - expect(queryMock.hasAncestor.calledOnce).equal(true); - expect(queryMock.ancestors.path).deep.equal(ancestors); - }); - }); + it('should call with ancestors', () => { + const ancestors = ['Parent', 'keyname']; - it('should call with namespace', () => { - const namespace = 'com.new-domain.dev'; + return GstoreModel.deleteAll(ancestors).then(() => { + expect(queryMock.hasAncestor.calledOnce).equal(true); + expect(queryMock.ancestors.path).deep.equal(ancestors); + }); + }); - return GstoreModel.deleteAll(null, namespace).then(() => { - expect(ds.createQuery.getCall(0).args[0]).equal(namespace); - }); - }); + it('should call with namespace', () => { + const namespace = 'com.new-domain.dev'; - it('should return success:true if all ok', () => GstoreModel.deleteAll().then(response => { - expect(response.success).equal(true); - })); + return GstoreModel.deleteAll(null, namespace).then(() => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); + }); + }); - it('should return error if any while deleting', () => { - const error = { code: 500, message: 'Could not delete' }; - sinon.stub(GstoreModel, 'delete').rejects(error); + it('should return success:true if all ok', () => + GstoreModel.deleteAll().then(response => { + expect(response.success).equal(true); + })); - return GstoreModel.deleteAll().catch(err => { - expect(err).equal(error); - }); - }); + it('should return error if any while deleting', () => { + const error = { code: 500, message: 'Could not delete' }; + sinon.stub(GstoreModel, 'delete').rejects(error); - it('should delete entites by batches of 500', done => { - ds.createQuery.restore(); + return GstoreModel.deleteAll().catch(err => { + expect(err).equal(error); + }); + }); - const entities = []; - const entity = { name: 'Mick', lastname: 'Jagger' }; - entity[ds.KEY] = ds.key(['BlogPost', 'keyname']); + it('should delete entites by batches of 500', done => { + ds.createQuery.restore(); - for (let i = 0; i < 1200; i += 1) { - entities.push(entity); - } + const entities = []; + const entity = { name: 'Mick', lastname: 'Jagger' }; + entity[ds.KEY] = ds.key(['BlogPost', 'keyname']); - const queryMock2 = new Query(ds, { entities }); - sinon.stub(ds, 'createQuery').callsFake(() => queryMock2); + for (let i = 0; i < 1200; i += 1) { + entities.push(entity); + } - GstoreModel.deleteAll().then(() => { - expect(false).equal(false); - done(); - }); - }); + const queryMock2 = new Query(ds, { entities }); + sinon.stub(ds, 'createQuery').callsFake(() => queryMock2); - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); - - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should delete all the keys from the cache and clear the Queries', done => { - ds.createQuery.restore(); - - const entities = []; - const entity = { name: 'Mick', lastname: 'Jagger' }; - entity[ds.KEY] = ds.key(['BlogPost', 'keyname']); - for (let i = 0; i < 1200; i += 1) { - entities.push(entity); - } - - queryMock = new Query(ds, { entities }); - sinon.stub(ds, 'createQuery').callsFake(() => ( - // Check - queryMock)); - sinon.spy(gstore.cache.keys, 'del'); - sinon.spy(gstore.cache.queries, 'clearQueriesByKind'); - - GstoreModel.deleteAll().then(() => { - expect(gstore.cache.queries.clearQueriesByKind.callCount).equal(1); - expect(gstore.cache.keys.del.callCount).equal(3); - const keys1 = gstore.cache.keys.del.getCall(0).args; - const keys2 = gstore.cache.keys.del.getCall(1).args; - const keys3 = gstore.cache.keys.del.getCall(2).args; - expect(keys1.length + keys2.length + keys3.length).equal(1200); - - gstore.cache.keys.del.restore(); - gstore.cache.queries.clearQueriesByKind.restore(); - done(); - }); - }); - }); + GstoreModel.deleteAll().then(() => { + expect(false).equal(false); + done(); + }); }); - describe('excludeFromIndexes', () => { - it('should add properties to schema as optional', () => { - const arr = ['newProp', 'url']; - GstoreModel.excludeFromIndexes(arr); - - const entity = new GstoreModel({}); - - expect(entity.excludeFromIndexes).deep.equal({ - lastname: ['lastname'], - age: ['age'], - newProp: ['newProp'], - url: ['url'], - }); - expect(schema.path('newProp').optional).equal(true); - }); + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should delete all the keys from the cache and clear the Queries', done => { + ds.createQuery.restore(); + + const entities = []; + const entity = { name: 'Mick', lastname: 'Jagger' }; + entity[ds.KEY] = ds.key(['BlogPost', 'keyname']); + for (let i = 0; i < 1200; i += 1) { + entities.push(entity); + } + + queryMock = new Query(ds, { entities }); + sinon.stub(ds, 'createQuery').callsFake( + () => + // Check + queryMock, + ); + sinon.spy(gstore.cache.keys, 'del'); + sinon.spy(gstore.cache.queries, 'clearQueriesByKind'); + + GstoreModel.deleteAll().then(() => { + expect(gstore.cache.queries.clearQueriesByKind.callCount).equal(1); + expect(gstore.cache.keys.del.callCount).equal(3); + const keys1 = gstore.cache.keys.del.getCall(0).args; + const keys2 = gstore.cache.keys.del.getCall(1).args; + const keys3 = gstore.cache.keys.del.getCall(2).args; + expect(keys1.length + keys2.length + keys3.length).equal(1200); + + gstore.cache.keys.del.restore(); + gstore.cache.queries.clearQueriesByKind.restore(); + done(); + }); + }); + }); + }); + + describe('excludeFromIndexes', () => { + it('should add properties to schema as optional', () => { + const arr = ['newProp', 'url']; + GstoreModel.excludeFromIndexes(arr); + + const entity = new GstoreModel({}); + + expect(entity.__excludeFromIndexes).deep.equal({ + lastname: ['lastname'], + age: ['age'], + newProp: ['newProp'], + url: ['url'], + }); + expect(schema.path('newProp').optional).equal(true); + }); - it('should only modifiy excludeFromIndexes on properties that already exist', () => { - const prop = 'lastname'; - GstoreModel.excludeFromIndexes(prop); + it('should only modifiy excludeFromIndexes on properties that already exist', () => { + const prop = 'lastname'; + GstoreModel.excludeFromIndexes(prop); - const entity = new GstoreModel({}); + const entity = new GstoreModel({}); - expect(entity.excludeFromIndexes).deep.equal({ - lastname: ['lastname'], - age: ['age'], - }); - assert.isUndefined(schema.path('lastname').optional); - expect(schema.path('lastname').excludeFromIndexes).equal(true); - }); + expect(entity.__excludeFromIndexes).deep.equal({ + lastname: ['lastname'], + age: ['age'], + }); + assert.isUndefined(schema.path('lastname').optional); + expect(schema.path('lastname').excludeFromIndexes).equal(true); }); + }); - describe('hooksTransaction()', () => { - beforeEach(() => { - delete transaction.hooks; - }); + describe('hooksTransaction()', () => { + beforeEach(() => { + delete transaction.hooks; + }); - it('should add hooks to a transaction', () => { - GstoreModel.hooksTransaction(transaction, [() => { }, () => { }]); + it('should add hooks to a transaction', () => { + GstoreModel.__hooksTransaction(transaction, [() => {}, () => {}]); - assert.isDefined(transaction.hooks.post); - expect(transaction.hooks.post.length).equal(2); - assert.isDefined(transaction.execPostHooks); - }); + assert.isDefined(transaction.hooks.post); + expect(transaction.hooks.post.length).equal(2); + assert.isDefined(transaction.execPostHooks); + }); - it('should not override previous hooks on transaction', () => { - const fn = () => { }; - transaction.hooks = { - post: [fn], - }; + it('should not override previous hooks on transaction', () => { + const fn = () => {}; + transaction.hooks = { + post: [fn], + }; - GstoreModel.hooksTransaction(transaction, [() => { }]); + GstoreModel.__hooksTransaction(transaction, [() => {}]); - expect(transaction.hooks.post[0]).equal(fn); - }); + expect(transaction.hooks.post[0]).equal(fn); + }); - it('--> execPostHooks() should chain each Promised hook from transaction', () => { - const postHook1 = sinon.stub().resolves(1); - const postHook2 = sinon.stub().resolves(2); - GstoreModel.hooksTransaction(transaction, [postHook1, postHook2]); + it('--> execPostHooks() should chain each Promised hook from transaction', () => { + const postHook1 = sinon.stub().resolves(1); + const postHook2 = sinon.stub().resolves(2); + GstoreModel.__hooksTransaction(transaction, [postHook1, postHook2]); - return transaction.execPostHooks().then(result => { - expect(postHook1.called).equal(true); - expect(postHook2.called).equal(true); - expect(result).equal(2); - }); - }); + return transaction.execPostHooks().then(result => { + expect(postHook1.called).equal(true); + expect(postHook2.called).equal(true); + expect(result).equal(2); + }); + }); - it('--> execPostHooks() should resolve if no hooks', () => { - GstoreModel.hooksTransaction(transaction, []); - delete transaction.hooks.post; + it('--> execPostHooks() should resolve if no hooks', () => { + GstoreModel.__hooksTransaction(transaction, []); + delete transaction.hooks.post; - return transaction.execPostHooks().then(() => { - expect(true).equal(true); - }); - }); + return transaction.execPostHooks().then(() => { + expect(true).equal(true); + }); }); + }); - describe('clearCache', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); + describe('clearCache', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); - afterEach(() => { - // empty the cache - gstore.cache.reset(); + afterEach(() => { + // empty the cache + gstore.cache.reset(); - if (gstore.cache.queries.clearQueriesByKind.restore) { - gstore.cache.queries.clearQueriesByKind.restore(); - } + if (gstore.cache.queries.clearQueriesByKind.restore) { + gstore.cache.queries.clearQueriesByKind.restore(); + } - delete gstore.cache; - }); + delete gstore.cache; + }); - it('should delete the cache', () => { - sinon.spy(gstore.cache.keys, 'del'); + it('should delete the cache', () => { + sinon.spy(gstore.cache.keys, 'del'); - return GstoreModel.clearCache([GstoreModel.key(112233), GstoreModel.key(778899)]) - .then(() => { - assert.ok(gstore.cache.keys.del.called); - expect(gstore.cache.keys.del.getCall(0).args[0].id).equal(112233); - expect(gstore.cache.keys.del.getCall(0).args[1].id).equal(778899); - gstore.cache.keys.del.restore(); - }); - }); + return GstoreModel.clearCache([GstoreModel.key(112233), GstoreModel.key(778899)]).then(() => { + assert.ok(gstore.cache.keys.del.called); + expect(gstore.cache.keys.del.getCall(0).args[0].id).equal(112233); + expect(gstore.cache.keys.del.getCall(0).args[1].id).equal(778899); + gstore.cache.keys.del.restore(); + }); + }); - it('should clear all queries linked to its entity kind', () => { - sinon.spy(gstore.cache.queries, 'clearQueriesByKind'); - return GstoreModel.clearCache() - .then(() => { - assert.ok(gstore.cache.queries.clearQueriesByKind.called); - const { args } = gstore.cache.queries.clearQueriesByKind.getCall(0); - expect(args[0]).equal(GstoreModel.entityKind); - }); - }); + it('should clear all queries linked to its entity kind', () => { + sinon.spy(gstore.cache.queries, 'clearQueriesByKind'); + return GstoreModel.clearCache().then(() => { + assert.ok(gstore.cache.queries.clearQueriesByKind.called); + const { args } = gstore.cache.queries.clearQueriesByKind.getCall(0); + expect(args[0]).equal(GstoreModel.entityKind); + }); + }); - it('should bubble up errors', done => { - const err = new Error('Houston something bad happened'); - sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - GstoreModel.clearCache(GstoreModel.key(123)) - .catch(e => { - expect(e).equal(err); - done(); - }); - }); + it('should bubble up errors', done => { + const err = new Error('Houston something bad happened'); + sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); + GstoreModel.clearCache(GstoreModel.key(123)).catch(e => { + expect(e).equal(err); + done(); + }); + }); - it('should not throw error if Redis is not present', () => { - const err = new Error('Redis store not founc'); - err.code = 'ERR_NO_REDIS'; - sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); + it('should not throw error if Redis is not present', () => { + const err = new Error('Redis store not founc'); + err.code = 'ERR_NO_REDIS'; + sinon.stub(gstore.cache.queries, 'clearQueriesByKind').rejects(err); - GstoreModel.clearCache(GstoreModel.key(123)) - .then(res => { - expect(res.success).equal(true); - }); - }); + GstoreModel.clearCache(GstoreModel.key(123)).then(res => { + expect(res.success).equal(true); + }); + }); + }); + + describe('populate()', () => { + let entity; + let key0; + let key1; + let key2; + let fetchData1; + let fetchData2; + let refs; + let entities; + + beforeEach(() => { + gstore.connect(ds); + schema = new Schema({ + name: { type: String }, + ref: { type: Schema.Types.Key }, + }); + GstoreModel = gstore.model('ModelTests-populate', schema, gstore); + + key0 = GstoreModel.key(123); + key1 = GstoreModel.key(456); + key2 = GstoreModel.key(789); + + entity = new GstoreModel({ name: 'Level0', ref: key1 }, null, null, null, key0); + + fetchData1 = { name: 'Level1', ref: key2 }; + fetchData1[ds.KEY] = key1; + + fetchData2 = { name: 'Level2' }; + fetchData2[ds.KEY] = key2; + + refs = [ + [{ path: 'ref', select: ['*'] }], // level 0 + [{ path: 'ref.ref', select: ['*'] }], // level 1 + ]; + entities = [entity]; + + const stub = sinon.stub(ds, 'get'); + stub.onCall(0).returns(Promise.resolve([fetchData1])); + stub.onCall(1).returns(Promise.resolve([fetchData2])); }); - describe('populate()', () => { - let entity; - let key0; - let key1; - let key2; - let fetchData1; - let fetchData2; - let refs; - let entities; - - beforeEach(() => { - gstore.connect(ds); - schema = new Schema({ - name: { type: String }, - ref: { type: Schema.Types.Key }, - }); - GstoreModel = gstore.model('ModelTests-populate', schema, gstore); - - key0 = GstoreModel.key(123); - key1 = GstoreModel.key(456); - key2 = GstoreModel.key(789); - - entity = new GstoreModel({ name: 'Level0', ref: key1 }, null, null, null, key0); - - fetchData1 = { name: 'Level1', ref: key2 }; - fetchData1[ds.KEY] = key1; - - fetchData2 = { name: 'Level2' }; - fetchData2[ds.KEY] = key2; - - refs = [ - [{ path: 'ref', select: ['*'] }], // level 0 - [{ path: 'ref.ref', select: ['*'] }], // level 1 - ]; - entities = [entity]; - - const stub = sinon.stub(ds, 'get'); - stub.onCall(0).returns(Promise.resolve([fetchData1])); - stub.onCall(1).returns(Promise.resolve([fetchData2])); - }); - - afterEach(() => { - ds.get.restore(); - }); + afterEach(() => { + ds.get.restore(); + }); - it('should recursively fetch the keys at each level of the entityData tree', () => ( - GstoreModel.populate(refs)(entities) - .then(({ 0: { entityData } }) => { - expect(entityData.ref.id).equal(456); - expect(entityData.ref.name).equal('Level1'); - expect(entityData.ref.ref.id).equal(789); - expect(entityData.ref.ref.name).equal('Level2'); - expect(ds.get.getCalls().length).equal(2); - }) + it('should recursively fetch the keys at each level of the entityData tree', () => + GstoreModel.__populate(refs)(entities).then(({ 0: { entityData } }) => { + expect(entityData.ref.id).equal(456); + expect(entityData.ref.name).equal('Level1'); + expect(entityData.ref.ref.id).equal(789); + expect(entityData.ref.ref.name).equal('Level2'); + expect(ds.get.getCalls().length).equal(2); + })); + + context('when cache is active', () => { + beforeEach(() => { + gstore.cache = gstoreWithCache.cache; + }); + + afterEach(() => { + // empty the cache + gstore.cache.reset(); + delete gstore.cache; + }); + + it('should get the keys from the cache and not fetch from the Datastore', () => + gstore.cache.keys.mset(key1, fetchData1, key2, fetchData2).then(() => + GstoreModel.__populate(refs)(entities).then(() => { + expect(ds.get.getCalls().length).equal(0); + }), )); - - context('when cache is active', () => { - beforeEach(() => { - gstore.cache = gstoreWithCache.cache; - }); - - afterEach(() => { - // empty the cache - gstore.cache.reset(); - delete gstore.cache; - }); - - it('should get the keys from the cache and not fetch from the Datastore', () => ( - gstore.cache.keys.mset(key1, fetchData1, key2, fetchData2) - .then(() => ( - GstoreModel.populate(refs)(entities) - .then(() => { - expect(ds.get.getCalls().length).equal(0); - }) - )) - )); - }); }); + }); }); diff --git a/test/query-test.js b/test/query-test.js index 8cf7744..aadbaf5 100644 --- a/test/query-test.js +++ b/test/query-test.js @@ -3,15 +3,13 @@ const chai = require('chai'); const sinon = require('sinon'); -const { Gstore } = require('../'); +const { Gstore, QUERIES_FORMATS } = require('../'); const ds = require('./mocks/datastore')({ - namespace: 'com.mydomain', + namespace: 'com.mydomain', }); const Transaction = require('./mocks/transaction'); const Query = require('./mocks/query'); const { generateEntities } = require('./mocks/entities'); -const { queryHelpers } = require('../lib/helpers'); -const Model = require('../lib/model'); const gstore = new Gstore(); const gstoreWithCache = new Gstore({ cache: true }); @@ -24,731 +22,658 @@ let responseQueries; let ModelInstance; const setupCacheContext = () => { - gstore.cache = gstoreWithCache.cache; + gstore.cache = gstoreWithCache.cache; - query = ModelInstance.query() - .filter('name', '=', 'John'); + query = ModelInstance.query().filter('name', '=', 'John'); - responseQueries = [mockEntities, { - moreResults: ds.MORE_RESULTS_AFTER_LIMIT, - endCursor: 'abcdef', - }]; + responseQueries = [ + mockEntities, + { + moreResults: ds.MORE_RESULTS_AFTER_LIMIT, + endCursor: 'abcdef', + }, + ]; - sinon.spy(gstore.cache.queries, 'read'); + sinon.spy(gstore.cache.queries, 'read'); }; const cleanupCacheContext = () => { - gstore.cache.reset(); - gstore.cache.queries.read.restore(); - delete gstore.cache; + gstore.cache.reset(); + gstore.cache.queries.read.restore(); + delete gstore.cache; }; describe('Query', () => { - let schema; - let transaction; + let schema; + let transaction; + + beforeEach(() => { + gstore.models = {}; + gstore.modelSchemas = {}; + gstore.options = {}; + gstore.cache = undefined; + + gstore.connect(ds); + gstoreWithCache.connect(ds); + + schema = new Schema({ + name: { type: 'string' }, + lastname: { type: 'string', excludeFromIndexes: true }, + password: { read: false }, + age: { type: 'int', excludeFromIndexes: true }, + birthday: { type: 'datetime' }, + street: {}, + website: { validate: 'isURL' }, + email: { validate: 'isEmail' }, + ip: { validate: { rule: 'isIP', args: [4] } }, + ip2: { validate: { rule: 'isIP' } }, // no args passed + modified: { type: 'boolean' }, + tags: { type: 'array' }, + prefs: { type: 'object' }, + price: { type: 'double', write: false }, + icon: { type: 'buffer' }, + location: { type: 'geoPoint' }, + }); + + ModelInstance = gstore.model('BlogTestQuery', schema); + transaction = new Transaction(); + + ({ mockEntities } = generateEntities()); + }); + + afterEach(() => { + if (query && query.__originalRun && query.__originalRun.restore) { + query.__originalRun.restore(); + } + }); + describe('gcloud-node queries', () => { beforeEach(() => { - gstore.models = {}; - gstore.modelSchemas = {}; - gstore.options = {}; - gstore.cache = undefined; - - gstore.connect(ds); - gstoreWithCache.connect(ds); - - schema = new Schema({ - name: { type: 'string' }, - lastname: { type: 'string', excludeFromIndexes: true }, - password: { read: false }, - age: { type: 'int', excludeFromIndexes: true }, - birthday: { type: 'datetime' }, - street: {}, - website: { validate: 'isURL' }, - email: { validate: 'isEmail' }, - ip: { validate: { rule: 'isIP', args: [4] } }, - ip2: { validate: { rule: 'isIP' } }, // no args passed - modified: { type: 'boolean' }, - tags: { type: 'array' }, - prefs: { type: 'object' }, - price: { type: 'double', write: false }, - icon: { type: 'buffer' }, - location: { type: 'geoPoint' }, - }); - - ModelInstance = gstore.model('Blog', schema, gstore); - transaction = new Transaction(); - - ({ mockEntities } = generateEntities()); + responseQueries = [ + mockEntities, + { + moreResults: ds.MORE_RESULTS_AFTER_LIMIT, + endCursor: 'abcdef', + }, + ]; + + query = ModelInstance.query(); + sinon.stub(query, '__originalRun').resolves(responseQueries); }); - afterEach(() => { - if (query && query.__originalRun && query.__originalRun.restore) { - query.__originalRun.restore(); - } + it('should create gcloud-node Query object', () => { + query = ModelInstance.query(); + + expect(query.constructor.name).equal('Query'); }); - describe('gcloud-node queries', () => { - beforeEach(() => { - responseQueries = [mockEntities, { - moreResults: ds.MORE_RESULTS_AFTER_LIMIT, - endCursor: 'abcdef', - }]; + it('should be able to execute all gcloud-node queries', () => { + const fn = () => { + query = ModelInstance.query() + .filter('name', '=', 'John') + .groupBy(['name']) + .select(['name']) + .order('lastname', { descending: true }) + .limit(1) + .offset(1) + .start('X'); + return query; + }; + + expect(fn).to.not.throw(Error); + }); - query = ModelInstance.query(); - sinon.stub(query, '__originalRun').resolves(responseQueries); - }); + it('should throw error if calling unregistered query method', () => { + const fn = () => { + query = ModelInstance.query().unkown('test', false); + return query; + }; - it('should create gcloud-node Query object', () => { - query = ModelInstance.query(); + expect(fn).to.throw(Error); + }); - expect(query.constructor.name).equal('Query'); - }); + it('should run query', () => + query.run().then(response => { + // We add manually the id in the mocks to be able to deep compare + mockEntities[0].id = 1234; + mockEntities[1].id = 'keyname'; + + // we delete from the mock the property + // 'password' it has been defined with read: false + delete mockEntities[0].password; + + expect(query.__originalRun.called).equal(true); + expect(response.entities.length).equal(2); + assert.isUndefined(response.entities[0].password); + expect(response.entities).deep.equal(mockEntities); + expect(response.nextPageCursor).equal('abcdef'); + + delete mockEntities[0].id; + delete mockEntities[1].id; + })); + + it('should add id to entities', () => + query.run().then(response => { + expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); + expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); + })); + + it('should accept "readAll" option', () => + query.run({ readAll: true }).then(response => { + assert.isDefined(response.entities[0].password); + })); + + it('should accept "showKey" option', () => + query.run({ showKey: true }).then(response => { + assert.isDefined(response.entities[0].__key); + })); + + it('should forward options to underlying Datastore.Query', () => + query.run({ consistency: 'strong' }).then(() => { + assert(query.__originalRun.called); + const { args } = query.__originalRun.getCall(0); + expect(args[0].consistency).equal('strong'); + })); + + it('should not add endCursor to response', () => { + query.__originalRun.restore(); + sinon.stub(query, '__originalRun').resolves([[], { moreResults: ds.NO_MORE_RESULTS }]); + + return query.run().then(response => { + assert.isUndefined(response.nextPageCursor); + }); + }); - it('should be able to execute all gcloud-node queries', () => { - const fn = () => { - query = ModelInstance.query() - .filter('name', '=', 'John') - .groupBy(['name']) - .select(['name']) - .order('lastname', { descending: true }) - .limit(1) - .offset(1) - .start('X'); - return query; - }; + it('should catch error thrown in query run()', () => { + const error = { code: 400, message: 'Something went wrong doctor' }; + query.__originalRun.restore(); + sinon.stub(query, '__originalRun').rejects(error); - expect(fn).to.not.throw(Error); - }); + return query.run().catch(err => { + expect(err).equal(error); + }); + }); - it('should throw error if calling unregistered query method', () => { - const fn = () => { - query = ModelInstance.query() - .unkown('test', false); - return query; - }; + it('should allow a namespace for query', () => { + const namespace = 'com.mydomain-dev'; + query = ModelInstance.query(namespace); - expect(fn).to.throw(Error); - }); + expect(query.namespace).equal(namespace); + }); - it('should run query', () => query.run().then(response => { - // We add manually the id in the mocks to be able to deep compare - mockEntities[0].id = 1234; - mockEntities[1].id = 'keyname'; + it('should create query on existing transaction', () => { + query = ModelInstance.query(null, transaction); + expect(query.scope.constructor.name).equal('Transaction'); + }); - // we delete from the mock the property - // 'password' it has been defined with read: false - delete mockEntities[0].password; + it('should not set transaction if not an instance of gcloud Transaction', () => { + const fn = () => { + query = ModelInstance.query(null, {}); + }; - expect(query.__originalRun.called).equal(true); - expect(response.entities.length).equal(2); - assert.isUndefined(response.entities[0].password); - expect(response.entities).deep.equal(mockEntities); - expect(response.nextPageCursor).equal('abcdef'); + expect(fn).to.throw(Error); + }); - delete mockEntities[0].id; - delete mockEntities[1].id; - })); + it('should still work with a callback', () => { + query = ModelInstance.query().filter('name', 'John'); + sinon.stub(query, '__originalRun').resolves(responseQueries); - it('should add id to entities', () => ( - query.run() - .then(response => { - expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); - expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); - }) - )); + return query.run((err, response) => { + expect(query.__originalRun.called).equal(true); + expect(response.entities.length).equal(2); + expect(response.nextPageCursor).equal('abcdef'); + }); + }); - it('should accept "readAll" option', () => ( - query.run({ readAll: true }) - .then(response => { - assert.isDefined(response.entities[0].password); - }) - )); + context('when cache is active', () => { + beforeEach(() => { + setupCacheContext(); + sinon.stub(query, '__originalRun').resolves(responseQueries); + }); + + afterEach(() => { + cleanupCacheContext(); + }); + + it('should get query from cache and pass down options', () => + query.run({ ttl: 9999 }).then(response => { + assert.ok(query.__originalRun.called); + expect(gstore.cache.queries.read.callCount).equal(1); + expect(gstore.cache.queries.read.getCall(0).args[1].ttl).equal(9999); + expect(response.entities[0].name).deep.equal(mockEntities[0].name); + expect(response.entities[1].name).deep.equal(mockEntities[1].name); + })); - it('should accept "showKey" option', () => ( - query.run({ showKey: true }) - .then(response => { - assert.isDefined(response.entities[0].__key); - }) + it('should *not* get query from cache', () => + gstore.cache.queries.set(query, responseQueries, { ttl: 600 }).then(() => + query.run({ cache: false }).then(() => { + assert.ok(query.__originalRun.called); + }), )); - it('should forward options to underlying Datastore.Query', () => ( - query.run({ consistency: 'strong' }) - .then(() => { - assert(query.__originalRun.called); - const { args } = query.__originalRun.getCall(0); - expect(args[0].consistency).equal('strong'); - }) - )); + it('should *not* get query from cache when ttl === -1', () => { + const conf = { ...gstore.cache.config.ttl }; + gstore.cache.config.ttl.queries = -1; + + return gstore.cache.queries.set(query, responseQueries, { ttl: 600 }).then(() => + query.run().then(() => { + expect(gstore.cache.queries.read.callCount).equal(0); + assert.ok(query.__originalRun.called); + gstore.cache.config.ttl = conf; // put back original config + }), + ); + }); + + it('should get query from the cache when ttl === -1 but option.cache is set to "true"', () => { + const conf = { ...gstore.cache.config.ttl }; + gstore.cache.config.ttl.queries = -1; + + return gstore.cache.queries.set(query, responseQueries, { ttl: 600 }).then(() => + query.run({ cache: true }).then(() => { + expect(gstore.cache.queries.read.callCount).equal(1); + assert.ok(!query.__originalRun.called); + gstore.cache.config.ttl = conf; // put back original config + }), + ); + }); + }); + }); - it('should not add endCursor to response', () => { - query.__originalRun.restore(); - sinon.stub(query, '__originalRun').resolves([[], { moreResults: ds.NO_MORE_RESULTS }]); + describe('shortcut queries', () => { + let queryMock; - return query.run().then(response => { - assert.isUndefined(response.nextPageCursor); - }); - }); + beforeEach(() => { + sinon.stub(ds, 'createQuery').callsFake(namespace => { + queryMock = new Query(ds, { entities: mockEntities }, undefined, namespace); + + sinon.spy(queryMock, 'run'); + sinon.spy(queryMock, 'filter'); + sinon.spy(queryMock, 'hasAncestor'); + sinon.spy(queryMock, 'order'); + sinon.spy(queryMock, 'limit'); + sinon.spy(queryMock, 'offset'); + + return queryMock; + }); + }); - it('should catch error thrown in query run()', () => { - const error = { code: 400, message: 'Something went wrong doctor' }; - query.__originalRun.restore(); - sinon.stub(query, '__originalRun').rejects(error); + afterEach(() => { + ds.createQuery.restore(); + if (!queryMock) { + return; + } + if (queryMock.run.restore) { + queryMock.run.restore(); + } + if (queryMock.filter.restore) { + queryMock.filter.restore(); + } + if (queryMock.hasAncestor.restore) { + queryMock.hasAncestor.restore(); + } + if (queryMock.order.restore) { + queryMock.order.restore(); + } + if (queryMock.limit.restore) { + queryMock.limit.restore(); + } + if (queryMock.offset.restore) { + queryMock.offset.restore(); + } + }); - return query.run().catch(err => { - expect(err).equal(error); - }); - }); + describe('list', () => { + it('should work with no settings defined', () => + ModelInstance.list().then(response => { + expect(response.entities.length).equal(2); + expect(response.nextPageCursor).equal('abcdef'); + assert.isUndefined(response.entities[0].password); + })); + + it('should add id to entities', () => + ModelInstance.list().then(response => { + expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); + expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); + })); - it('should allow a namespace for query', () => { - const namespace = 'com.mydomain-dev'; - query = ModelInstance.query(namespace); + it('should not add endCursor to response', () => { + ds.createQuery.restore(); + sinon + .stub(ds, 'createQuery') + .callsFake(() => new Query(ds, { entities: mockEntities }, { moreResults: ds.NO_MORE_RESULTS })); - expect(query.namespace).equal(namespace); + return ModelInstance.list().then(response => { + assert.isUndefined(response.nextPageCursor); }); - - it('should create query on existing transaction', () => { - query = ModelInstance.query(null, transaction); - expect(query.scope.constructor.name).equal('Transaction'); + }); + + it('should read settings passed', () => { + const querySettings = { + limit: 10, + offset: 10, + format: QUERIES_FORMATS.ENTITY, + }; + schema.queries('list', querySettings); + ModelInstance = gstore.model('Blog', schema); + + return ModelInstance.list().then(response => { + expect(queryMock.limit.getCall(0).args[0]).equal(querySettings.limit); + expect(queryMock.offset.getCall(0).args[0]).equal(querySettings.offset); + expect(response.entities[0].__className).equal('Entity'); + }); + }); + + it('should override global setting with options', () => { + const querySettings = { + limit: 10, + offset: 10, + readAll: true, + showKey: true, + }; + schema.queries('list', querySettings); + ModelInstance = gstore.model('Blog', schema); + + return ModelInstance.list({ limit: 15, offset: 15 }).then(response => { + expect(queryMock.limit.getCall(0).args[0]).equal(15); + expect(queryMock.offset.getCall(0).args[0]).equal(15); + assert.isDefined(response.entities[0].password); + assert.isDefined(response.entities[0].__key); + }); + }); + + it('should deal with err response', () => { + const error = { code: 500, message: 'Server error' }; + ds.createQuery.callsFake(() => { + queryMock = new Query(ds, { entities: mockEntities }); + sinon.stub(queryMock, 'run').rejects(error); + return queryMock; }); - it('should not set transaction if not an instance of gcloud Transaction', () => { - const fn = () => { - query = ModelInstance.query(null, {}); - }; - - expect(fn).to.throw(Error); + return ModelInstance.list().catch(err => { + expect(err).equal(err); }); + }); - it('should still work with a callback', () => { - query = ModelInstance.query() - .filter('name', 'John'); - sinon.stub(query, '__originalRun').resolves(responseQueries); + it('should accept a namespace ', () => { + const namespace = 'com.mydomain-dev'; - return query.run((err, response) => { - expect(query.__originalRun.called).equal(true); - expect(response.entities.length).equal(2); - expect(response.nextPageCursor).equal('abcdef'); - }); + return ModelInstance.list({ namespace }).then(() => { + expect(queryMock.namespace).equal(namespace); }); + }); - context('when cache is active', () => { - beforeEach(() => { - setupCacheContext(); - sinon.stub(query, '__originalRun').resolves(responseQueries); - }); + context('when cache is active', () => { + beforeEach(() => { + setupCacheContext(); + }); - afterEach(() => { - cleanupCacheContext(); - }); + afterEach(() => { + cleanupCacheContext(); + }); - it('should get query from cache and pass down options', () => ( - query.run({ ttl: 9999 }) - .then(response => { - assert.ok(query.__originalRun.called); - expect(gstore.cache.queries.read.callCount).equal(1); - expect(gstore.cache.queries.read.getCall(0).args[1].ttl).equal(9999); - expect(response.entities[0].name).deep.equal(mockEntities[0].name); - expect(response.entities[1].name).deep.equal(mockEntities[1].name); - }) - )); - - it('should *not* get query from cache', () => ( - gstore.cache.queries.set(query, responseQueries, { ttl: 600 }) - .then(() => ( - query.run({ cache: false }) - .then(() => { - assert.ok(query.__originalRun.called); - }) - )) - )); - - it('should *not* get query from cache when ttl === -1', () => { - const conf = { ...gstore.cache.config.ttl }; - gstore.cache.config.ttl.queries = -1; - - return gstore.cache.queries.set(query, responseQueries, { ttl: 600 }) - .then(() => ( - query.run() - .then(() => { - expect(gstore.cache.queries.read.callCount).equal(0); - assert.ok(query.__originalRun.called); - gstore.cache.config.ttl = conf; // put back original config - }) - )); - }); + it('should get query from cache and pass down options', () => { + const options = { ttl: 7777, cache: true }; - it('should get query from the cache when ttl === -1 but option.cache is set to "true"', () => { - const conf = { ...gstore.cache.config.ttl }; - gstore.cache.config.ttl.queries = -1; - - return gstore.cache.queries.set(query, responseQueries, { ttl: 600 }) - .then(() => ( - query.run({ cache: true }) - .then(() => { - expect(gstore.cache.queries.read.callCount).equal(1); - assert.ok(!query.__originalRun.called); - gstore.cache.config.ttl = conf; // put back original config - }) - )); - }); + return ModelInstance.list(options).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); + expect(ModelInstance.gstore.cache.queries.read.getCall(0).args[1]).contains(options); + }); }); - }); - describe('shortcut queries', () => { - let queryMock; + it('should *not* get query from cache', () => + ModelInstance.list({ cache: false }).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); + })); + }); + }); - beforeEach(() => { - sinon.stub(ds, 'createQuery').callsFake(() => { - queryMock = new Query(ds, { entities: mockEntities }); + describe('findAround()', () => { + it('should get 3 entities after a given date', () => + ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }).then(entities => { + expect(queryMock.filter.getCall(0).args).deep.equal(['createdOn', '>', '2016-1-1']); + expect(queryMock.order.getCall(0).args).deep.equal(['createdOn', { descending: true }]); + expect(queryMock.limit.getCall(0).args[0]).equal(3); - sinon.spy(queryMock, 'run'); - sinon.spy(queryMock, 'filter'); - sinon.spy(queryMock, 'hasAncestor'); - sinon.spy(queryMock, 'order'); - sinon.spy(queryMock, 'limit'); - sinon.spy(queryMock, 'offset'); + // Make sure to not show properties where read is set to false + assert.isUndefined(entities[0].password); + })); - return queryMock; - }); + it('should get 3 entities before a given date', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 12 }).then(() => { + expect(queryMock.filter.getCall(0).args).deep.equal(['createdOn', '<', '2016-1-1']); + expect(queryMock.limit.getCall(0).args[0]).equal(12); + })); - sinon.spy(queryHelpers, 'buildFromOptions'); + it('should throw error if not all arguments are passed', done => { + ModelInstance.findAround('createdOn', '2016-1-1').catch(err => { + expect(err.message).equal('[gstore.findAround()]: Not all the arguments were provided.'); + done(); }); + }); - afterEach(() => { - ds.createQuery.restore(); - queryHelpers.buildFromOptions.restore(); - if (queryMock.run.restore) { - queryMock.run.restore(); - } - if (queryMock.filter.restore) { - queryMock.filter.restore(); - } - if (queryMock.hasAncestor.restore) { - queryMock.hasAncestor.restore(); - } - if (queryMock.order.restore) { - queryMock.order.restore(); - } - if (queryMock.limit.restore) { - queryMock.limit.restore(); - } - if (queryMock.offset.restore) { - queryMock.offset.restore(); - } - }); - - describe('list', () => { - it('should work with no settings defined', () => ( - ModelInstance.list().then(response => { - expect(response.entities.length).equal(2); - expect(response.nextPageCursor).equal('abcdef'); - assert.isUndefined(response.entities[0].password); - }) - )); - - it('should add id to entities', () => ( - ModelInstance.list().then(response => { - expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); - expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); - }) - )); - - it('should not add endCursor to response', () => { - ds.createQuery.restore(); - sinon.stub(ds, 'createQuery').callsFake(() => ( - new Query(ds, { entities: mockEntities }, { moreResults: ds.NO_MORE_RESULTS }))); - - return ModelInstance.list().then(response => { - assert.isUndefined(response.nextPageCursor); - }); - }); + it('should validate that options passed is an object', done => { + ModelInstance.findAround('createdOn', '2016-1-1', 'string').catch(err => { + expect(err.message).equal('[gstore.findAround()]: Options pased has to be an object.'); + done(); + }); + }); - it('should read settings passed', () => { - const querySettings = { - limit: 10, - offset: 10, - format: gstore.Queries.formats.ENTITY, - }; - schema.queries('list', querySettings); - ModelInstance = Model.compile('Blog', schema, gstore); - - return ModelInstance.list().then(response => { - expect(queryHelpers.buildFromOptions.getCall(0).args[1].limit).equal(querySettings.limit); - expect(queryMock.limit.getCall(0).args[0]).equal(querySettings.limit); - expect(queryHelpers.buildFromOptions.getCall(0).args[1].offset).equal(querySettings.offset); - expect(queryMock.offset.getCall(0).args[0]).equal(querySettings.offset); - expect(response.entities[0].className).equal('Entity'); - }); - }); + it('should validate that options has an "after" or "before" property', done => { + ModelInstance.findAround('createdOn', '2016-1-1', {}).catch(err => { + expect(err.message).equal('[gstore.findAround()]: You must set "after" or "before" in options.'); + done(); + }); + }); - it('should override global setting with options', () => { - const querySettings = { - limit: 10, - offset: 10, - readAll: true, - showKey: true, - }; - schema.queries('list', querySettings); - ModelInstance = Model.compile('Blog', schema, gstore); - - return ModelInstance.list({ limit: 15, offset: 15 }).then(response => { - expect(queryHelpers.buildFromOptions.getCall(0).args[1]).not.deep.equal(querySettings); - expect(queryMock.limit.getCall(0).args[0]).equal(15); - expect(queryMock.offset.getCall(0).args[0]).equal(15); - assert.isDefined(response.entities[0].password); - assert.isDefined(response.entities[0].__key); - }); - }); + it('should validate that options has not both "after" & "before" properties', done => { + ModelInstance.findAround('createdOn', '2016-1-1', { after: 3, before: 3 }).catch(err => { + expect(err.message).equal('[gstore.findAround()]: You can\'t set both "after" and "before".'); + done(); + }); + }); - it('should deal with err response', () => { - const error = { code: 500, message: 'Server error' }; - ds.createQuery.callsFake(() => { - queryMock = new Query(ds, { entities: mockEntities }); - sinon.stub(queryMock, 'run').rejects(error); - return queryMock; - }); - - return ModelInstance.list().catch(err => { - expect(err).equal(err); - }); - }); + it('should add id to entities', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }).then(entities => { + expect(entities[0].id).equal(mockEntities[0][ds.KEY].id); + expect(entities[1].id).equal(mockEntities[1][ds.KEY].name); + })); - it('should accept a namespace ', () => { - const namespace = 'com.mydomain-dev'; + it('should read all properties', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3, readAll: true, format: 'ENTITY' }).then( + entities => { + assert.isDefined(entities[0].password); + expect(entities[0].__className).equal('Entity'); + }, + )); - return ModelInstance.list({ namespace }).then(() => { - expect(queryHelpers.buildFromOptions.getCall(0).args[1]).deep.equal({ namespace }); - }); - }); + it('should add entities key', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3, showKey: true }).then(entities => { + assert.isDefined(entities[0].__key); + })); - it('should call Model.query() to create Datastore Query', () => { - const namespace = 'com.mydomain-dev'; - sinon.spy(ModelInstance, 'query'); + it('should accept a namespace', () => { + const namespace = 'com.new-domain.dev'; + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }, namespace).then(() => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); + }); + }); - return ModelInstance.list({ namespace }).then(() => { - expect(ModelInstance.query.getCall(0).args[0]).deep.equal(namespace); - }); - }); + it('should deal with err response', () => { + const error = { code: 500, message: 'Server error' }; - context('when cache is active', () => { - beforeEach(() => { - setupCacheContext(); - }); - - afterEach(() => { - cleanupCacheContext(); - }); - - it('should get query from cache and pass down options', () => { - const options = { ttl: 7777, cache: true }; - - return ModelInstance.list(options).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); - expect(ModelInstance.gstore.cache.queries.read.getCall(0).args[1]).contains(options); - }); - }); - - it('should *not* get query from cache', () => ( - ModelInstance.list({ cache: false }).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); - }) - )); - }); + ds.createQuery.callsFake(() => { + queryMock = new Query(ds, { entities: mockEntities }); + sinon.stub(queryMock, 'run').rejects(error); + return queryMock; }); - describe('findAround()', () => { - it('should call Model.query() to create Datastore Query', () => { - const namespace = 'com.mydomain-dev'; - sinon.spy(ModelInstance, 'query'); + return ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }).catch(err => { + expect(err).equal(error); + }); + }); - return ModelInstance.findAround('xxx', 'xxx', { after: 3 }, namespace) - .then(() => { - expect(ModelInstance.query.getCall(0).args[0]).deep.equal(namespace); - }); - }); + context('when cache is active', () => { + beforeEach(() => { + setupCacheContext(); + }); - it('should get 3 entities after a given date', () => ( - ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }) - .then(entities => { - expect(queryMock.filter.getCall(0).args) - .deep.equal(['createdOn', '>', '2016-1-1']); - expect(queryMock.order.getCall(0).args) - .deep.equal(['createdOn', { descending: true }]); - expect(queryMock.limit.getCall(0).args[0]).equal(3); - - // Make sure to not show properties where read is set to false - assert.isUndefined(entities[0].password); - }) - )); - - it('should get 3 entities before a given date', () => ( - ModelInstance.findAround('createdOn', '2016-1-1', { before: 12 }).then(() => { - expect(queryMock.filter.getCall(0).args).deep.equal(['createdOn', '<', '2016-1-1']); - expect(queryMock.limit.getCall(0).args[0]).equal(12); - }) - )); - - it('should throw error if not all arguments are passed', done => { - ModelInstance.findAround('createdOn', '2016-1-1') - .catch(err => { - expect(err.message).equal('[gstore.findAround()]: Not all the arguments were provided.'); - done(); - }); - }); + afterEach(() => { + cleanupCacheContext(); + }); - it('should validate that options passed is an object', done => { - ModelInstance.findAround('createdOn', '2016-1-1', 'string') - .catch(err => { - expect(err.message).equal('[gstore.findAround()]: Options pased has to be an object.'); - done(); - }); - }); + it('should get query from cache and pass down options', () => { + const options = { ttl: 7777, cache: true, after: 3 }; - it('should validate that options has an "after" or "before" property', done => { - ModelInstance.findAround('createdOn', '2016-1-1', {}) - .catch(err => { - expect(err.message) - .equal('[gstore.findAround()]: You must set "after" or "before" in options.'); - done(); - }); - }); + return ModelInstance.findAround('xxx', 'xxx', options).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); + const { args } = ModelInstance.gstore.cache.queries.read.getCall(0); + expect(args[1]).contains({ ttl: 7777, cache: true }); + }); + }); - it('should validate that options has not both "after" & "before" properties', done => { - ModelInstance.findAround('createdOn', '2016-1-1', { after: 3, before: 3 }) - .catch(err => { - expect(err.message).equal('[gstore.findAround()]: You can\'t set both "after" and "before".'); - done(); - }); - }); + it('should *not* get query from cache', () => + ModelInstance.findAround('xxx', 'xxx', { after: 3, cache: false }).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); + })); + }); + }); - it('should add id to entities', () => ( - ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }).then(entities => { - expect(entities[0].id).equal(mockEntities[0][ds.KEY].id); - expect(entities[1].id).equal(mockEntities[1][ds.KEY].name); - }) - )); - - it('should read all properties', () => ( - ModelInstance.findAround('createdOn', '2016-1-1', { before: 3, readAll: true, format: 'ENTITY' }) - .then(entities => { - assert.isDefined(entities[0].password); - expect(entities[0].className).equal('Entity'); - }) - )); - - it('should add entities key', () => ( - ModelInstance.findAround('createdOn', '2016-1-1', { before: 3, showKey: true }) - .then(entities => { - assert.isDefined(entities[0].__key); - }) - )); - - it('should accept a namespace', () => { - const namespace = 'com.new-domain.dev'; - ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }, namespace).then(() => { - expect(ds.createQuery.getCall(0).args[0]).equal(namespace); - }); - }); + describe('findOne()', () => { + it('should call pre and post hooks', () => { + const spies = { + pre: () => Promise.resolve(), + post: () => Promise.resolve(), + }; + sinon.spy(spies, 'pre'); + sinon.spy(spies, 'post'); + schema.pre('findOne', spies.pre); + schema.post('findOne', spies.post); + ModelInstance = gstore.model('Blog', schema); + + return ModelInstance.findOne({}).then(() => { + expect(spies.pre.calledOnce).equal(true); + expect(spies.post.calledOnce).equal(true); + expect(spies.pre.calledBefore(queryMock.__originalRun)).equal(true); + expect(spies.post.calledAfter(queryMock.__originalRun)).equal(true); + }); + }); - it('should deal with err response', () => { - const error = { code: 500, message: 'Server error' }; + it('should run correct gcloud Query', () => + ModelInstance.findOne({ name: 'John', email: 'john@snow.com' }).then(() => { + expect(queryMock.filter.getCall(0).args).deep.equal(['name', 'John']); - ds.createQuery.callsFake(() => { - queryMock = new Query(ds, { entities: mockEntities }); - sinon.stub(queryMock, 'run').rejects(error); - return queryMock; - }); + expect(queryMock.filter.getCall(1).args).deep.equal(['email', 'john@snow.com']); + })); - return ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }).catch(err => { - expect(err).equal(error); - }); - }); + it('should return a Model instance', () => + ModelInstance.findOne({ name: 'John' }).then(entity => { + expect(entity.entityKind).equal('BlogTestQuery'); + expect(entity.__className).equal('Entity'); + })); - context('when cache is active', () => { - beforeEach(() => { - setupCacheContext(); - }); - - afterEach(() => { - cleanupCacheContext(); - }); - - it('should get query from cache and pass down options', () => { - const options = { ttl: 7777, cache: true, after: 3 }; - - return ModelInstance.findAround('xxx', 'xxx', options).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); - const { args } = ModelInstance.gstore.cache.queries.read.getCall(0); - expect(args[1]).contains({ ttl: 7777, cache: true }); - }); - }); - - it('should *not* get query from cache', () => ( - ModelInstance.findAround('xxx', 'xxx', { after: 3, cache: false }).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); - }) - )); - }); + it('should validate that params passed are object', done => { + ModelInstance.findOne('some string').catch(err => { + expect(err.message).equal('[gstore.findOne()]: "Params" has to be an object.'); + done(); }); + }); - describe('findOne()', () => { - it('should call Model.query() to create Datastore Query', () => { - const namespace = 'com.mydomain-dev'; - sinon.spy(ModelInstance, 'query'); + it('should accept ancestors', () => { + const ancestors = ['Parent', 'keyname']; - return ModelInstance.findOne({ name: 'John' }, null, namespace) - .then(() => { - expect(ModelInstance.query.getCall(0).args[0]).deep.equal(namespace); - }); - }); + return ModelInstance.findOne({ name: 'John' }, ancestors, () => { + expect(queryMock.hasAncestor.getCall(0).args[0].path).deep.equal(ancestors); + }); + }); - it('should call pre and post hooks', () => { - const spies = { - pre: () => Promise.resolve(), - post: () => Promise.resolve(), - }; - sinon.spy(spies, 'pre'); - sinon.spy(spies, 'post'); - schema.pre('findOne', spies.pre); - schema.post('findOne', spies.post); - ModelInstance = Model.compile('Blog', schema, gstore); - - return ModelInstance.findOne({}).then(() => { - expect(spies.pre.calledOnce).equal(true); - expect(spies.post.calledOnce).equal(true); - expect(spies.pre.calledBefore(queryMock.__originalRun)).equal(true); - expect(spies.post.calledAfter(queryMock.__originalRun)).equal(true); - }); - }); + it('should accept a namespace', () => { + const namespace = 'com.new-domain.dev'; - it('should run correct gcloud Query', () => ( - ModelInstance.findOne({ name: 'John', email: 'john@snow.com' }).then(() => { - expect(queryMock.filter.getCall(0).args) - .deep.equal(['name', 'John']); - - expect(queryMock.filter.getCall(1).args) - .deep.equal(['email', 'john@snow.com']); - }) - )); - - it('should return a Model instance', () => ( - ModelInstance.findOne({ name: 'John' }).then(entity => { - expect(entity.entityKind).equal('Blog'); - expect(entity instanceof Model).equal(true); - }) - )); - - it('should validate that params passed are object', done => { - ModelInstance.findOne('some string') - .catch(err => { - expect(err.message).equal('[gstore.findOne()]: "Params" has to be an object.'); - done(); - }); - }); + return ModelInstance.findOne({ name: 'John' }, null, namespace).then(() => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); + }); + }); - it('should accept ancestors', () => { - const ancestors = ['Parent', 'keyname']; + it('should deal with err response', () => { + const error = { code: 500, message: 'Server error' }; - return ModelInstance.findOne({ name: 'John' }, ancestors, () => { - expect(queryMock.hasAncestor.getCall(0).args[0].path) - .deep.equal(ancestors); - }); - }); + ds.createQuery.callsFake(() => { + queryMock = new Query(ds, { entities: mockEntities }); + sinon.stub(queryMock, 'run').rejects(error); + return queryMock; + }); - it('should accept a namespace', () => { - const namespace = 'com.new-domain.dev'; + return ModelInstance.findOne({ name: 'John' }).catch(err => { + expect(err).equal(error); + }); + }); - return ModelInstance.findOne({ name: 'John' }, null, namespace) - .then(() => { - expect(ds.createQuery.getCall(0).args[0]).equal(namespace); - }); - }); + it('if entity not found should return "ERR_ENTITY_NOT_FOUND"', () => { + ds.createQuery.callsFake(() => { + queryMock = new Query(ds, { entities: [] }); + return queryMock; + }); - it('should deal with err response', () => { - const error = { code: 500, message: 'Server error' }; + return ModelInstance.findOne({ name: 'John' }).catch(err => { + expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); + }); + }); + + it('should call pre hooks and override parameters', () => { + const spyPre = sinon.stub().callsFake((...args) => { + // Make sure the original arguments are passed to the hook + if (args[0].name === 'John') { + // And override them + return Promise.resolve({ + __override: [{ name: 'Mick', email: 'mick@jagger.com' }, ['Parent', 'default']], + }); + } + return Promise.resolve(); + }); - ds.createQuery.callsFake(() => { - queryMock = new Query(ds, { entities: mockEntities }); - sinon.stub(queryMock, 'run').rejects(error); - return queryMock; - }); + schema = new Schema({ name: { type: 'string' } }); + schema.pre('findOne', function preHook(...args) { + return spyPre.apply(this, args); + }); - return ModelInstance.findOne({ name: 'John' }).catch(err => { - expect(err).equal(error); - }); - }); + ModelInstance = gstore.model('Blog', schema); - it('if entity not found should return "ERR_ENTITY_NOT_FOUND"', () => { - ds.createQuery.callsFake(() => { - queryMock = new Query(ds, { entities: [] }); - return queryMock; - }); + return ModelInstance.findOne({ name: 'John', email: 'john@snow.com' }).then(() => { + assert.ok(spyPre.calledBefore(ds.createQuery)); + const { args } = queryMock.filter.getCall(0); + const { args: args2 } = queryMock.filter.getCall(1); + const { args: args3 } = queryMock.hasAncestor.getCall(0); - return ModelInstance.findOne({ name: 'John' }).catch(err => { - expect(err.code).equal(gstore.errors.codes.ERR_ENTITY_NOT_FOUND); - }); - }); + expect(args[0]).equal('name'); + expect(args[1]).equal('Mick'); + expect(args2[0]).equal('email'); + expect(args2[1]).equal('mick@jagger.com'); + expect(args3[0].kind).equal('Parent'); + expect(args3[0].name).equal('default'); + }); + }); - it('should call pre hooks and override parameters', () => { - const spyPre = sinon.stub().callsFake((...args) => { - // Make sure the original arguments are passed to the hook - if (args[0].name === 'John') { - // And override them - return Promise.resolve({ - __override: [ - { name: 'Mick', email: 'mick@jagger.com' }, - ['Parent', 'default'], - ], - }); - } - return Promise.resolve(); - }); - - schema = new Schema({ name: { type: 'string' } }); - schema.pre('findOne', function preHook(...args) { - return spyPre.apply(this, args); - }); - - ModelInstance = Model.compile('Blog', schema, gstore); - - return ModelInstance.findOne({ name: 'John', email: 'john@snow.com' }).then(() => { - assert.ok(spyPre.calledBefore(ds.createQuery)); - const { args } = queryMock.filter.getCall(0); - const { args: args2 } = queryMock.filter.getCall(1); - const { args: args3 } = queryMock.hasAncestor.getCall(0); - - expect(args[0]).equal('name'); - expect(args[1]).equal('Mick'); - expect(args2[0]).equal('email'); - expect(args2[1]).equal('mick@jagger.com'); - expect(args3[0].kind).equal('Parent'); - expect(args3[0].name).equal('default'); - }); - }); + context('when cache is active', () => { + beforeEach(() => { + setupCacheContext(); + }); - context('when cache is active', () => { - beforeEach(() => { - setupCacheContext(); - }); - - afterEach(() => { - cleanupCacheContext(); - }); - - it('should get query from cache and pass down options', () => ( - ModelInstance.findOne({ name: 'John' }, null, null, { ttl: 7777, cache: true }).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); - const { args } = ModelInstance.gstore.cache.queries.read.getCall(0); - expect(args[1]).contains({ ttl: 7777, cache: true }); - }) - )); - - it('should *not* get query from cache', () => ( - ModelInstance.findOne({ name: 'John' }, null, null, { cache: false }).then(() => { - expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); - }) - )); - }); + afterEach(() => { + cleanupCacheContext(); }); + + it('should get query from cache and pass down options', () => + ModelInstance.findOne({ name: 'John' }, null, null, { ttl: 7777, cache: true }).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(1); + const { args } = ModelInstance.gstore.cache.queries.read.getCall(0); + expect(args[1]).contains({ ttl: 7777, cache: true }); + })); + + it('should *not* get query from cache', () => + ModelInstance.findOne({ name: 'John' }, null, null, { cache: false }).then(() => { + expect(ModelInstance.gstore.cache.queries.read.callCount).equal(0); + })); + }); }); + }); }); diff --git a/test/schema-test.js b/test/schema-test.js index ed24155..767579a 100644 --- a/test/schema-test.js +++ b/test/schema-test.js @@ -13,198 +13,204 @@ gstore.connect(ds); const { expect, assert } = chai; describe('Schema', () => { - describe('contructor', () => { - it('should initialized properties', () => { - const schema = new Schema({}); - - assert.isDefined(schema.methods); - assert.isDefined(schema.shortcutQueries); - assert.isDefined(schema.paths); - assert.isDefined(schema.callQueue); - assert.isDefined(schema.options); - expect(schema.options.queries).deep.equal({ readAll: false, format: 'JSON' }); - }); - - it('should merge options passed', () => { - const schema = new Schema({}, { - newOption: 'myValue', - queries: { simplifyResult: false }, - }); - - expect(schema.options.newOption).equal('myValue'); - expect(schema.options.queries.simplifyResult).equal(false); - }); - - it('should create its paths from obj passed', () => { - const schema = new Schema({ - property1: { type: 'string' }, - property2: { type: 'number' }, - }); + describe('contructor', () => { + it('should initialized properties', () => { + const schema = new Schema({}); + + assert.isDefined(schema.methods); + assert.isDefined(schema.shortcutQueries); + assert.isDefined(schema.paths); + assert.isDefined(schema.__callQueue); + assert.isDefined(schema.options); + expect(schema.options.queries).deep.equal({ readAll: false, format: 'JSON' }); + }); - assert.isDefined(schema.paths.property1); - assert.isDefined(schema.paths.property2); - }); + it('should merge options passed', () => { + const schema = new Schema( + {}, + { + newOption: 'myValue', + queries: { simplifyResult: false }, + }, + ); + + expect(schema.options.newOption).equal('myValue'); + expect(schema.options.queries.simplifyResult).equal(false); + }); - it('should not allowed reserved properties on schema', () => { - const fn = () => { - const schema = new Schema({ ds: 123 }); - return schema; - }; + it('should create its paths from obj passed', () => { + const schema = new Schema({ + property1: { type: 'string' }, + property2: { type: 'number' }, + }); - expect(fn).to.throw(Error); - }); + assert.isDefined(schema.paths.property1); + assert.isDefined(schema.paths.property2); }); - describe('add method', () => { - let schema; + it('should not allowed reserved properties on schema', () => { + const fn = () => { + const schema = new Schema({ ds: 123 }); + return schema; + }; - beforeEach(() => { - schema = new Schema({}); - schema.methods = {}; - }); + expect(fn).to.throw(Error); + }); + }); - it('should add it to its methods table', () => { - const fn = () => { }; - schema.method('doSomething', fn); + describe('add method', () => { + let schema; - assert.isDefined(schema.methods.doSomething); - expect(schema.methods.doSomething).to.equal(fn); - }); + beforeEach(() => { + schema = new Schema({}); + schema.methods = {}; + }); - it('should not do anything if value passed is not a function', () => { - schema.method('doSomething', 123); + it('should add it to its methods table', () => { + const fn = () => {}; + schema.method('doSomething', fn); - assert.isUndefined(schema.methods.doSomething); - }); + assert.isDefined(schema.methods.doSomething); + expect(schema.methods.doSomething).to.equal(fn); + }); - it('should allow to pass a table of functions and validate type', () => { - const fn = () => { }; - schema.method({ - doSomething: fn, - doAnotherThing: 123, - }); + it('should not do anything if value passed is not a function', () => { + schema.method('doSomething', 123); - assert.isDefined(schema.methods.doSomething); - expect(schema.methods.doSomething).to.equal(fn); - assert.isUndefined(schema.methods.doAnotherThing); - }); + assert.isUndefined(schema.methods.doSomething); + }); - it('should only allow function and object to be passed', () => { - schema.method(10, () => { }); + it('should allow to pass a table of functions and validate type', () => { + const fn = () => {}; + schema.method({ + doSomething: fn, + doAnotherThing: 123, + }); - expect(Object.keys(schema.methods).length).equal(0); - }); + assert.isDefined(schema.methods.doSomething); + expect(schema.methods.doSomething).to.equal(fn); + assert.isUndefined(schema.methods.doAnotherThing); }); - describe('modify / access paths table', () => { - it('should read', () => { - const data = { keyname: { type: 'string' } }; - const schema = new Schema(data); + it('should only allow function and object to be passed', () => { + schema.method(10, () => {}); - const pathValue = schema.path('keyname'); - - expect(pathValue).to.equal(data.keyname); - }); + expect(Object.keys(schema.methods).length).equal(0); + }); + }); - it('should not return anything if does not exist', () => { - const schema = new Schema({}); + describe('modify / access paths table', () => { + it('should read', () => { + const data = { keyname: { type: 'string' } }; + const schema = new Schema(data); - const pathValue = schema.path('keyname'); + const pathValue = schema.path('keyname'); - assert.isUndefined(pathValue); - }); + expect(pathValue).to.equal(data.keyname); + }); - it('should set', () => { - const schema = new Schema({}); - schema.path('keyname', { type: 'string' }); + it('should not return anything if does not exist', () => { + const schema = new Schema({}); - assert.isDefined(schema.paths.keyname); - }); + const pathValue = schema.path('keyname'); - it('should not allow to set reserved key', () => { - const schema = new Schema({}); + assert.isUndefined(pathValue); + }); - const fn = () => { - schema.path('ds', {}); - }; + it('should set', () => { + const schema = new Schema({}); + schema.path('keyname', { type: 'string' }); - expect(fn).to.throw(Error); - }); + assert.isDefined(schema.paths.keyname); }); - describe('callQueue', () => { - it('should add pre hooks to callQueue', () => { - const preMiddleware = () => { }; - const schema = new Schema({}); - schema.callQueue = { model: {}, entity: {} }; + it('should not allow to set reserved key', () => { + const schema = new Schema({}); - schema.pre('save', preMiddleware); - schema.pre('save', preMiddleware); // we add 2 so we test both cases L140 + const fn = () => { + schema.path('ds', {}); + }; - assert.isDefined(schema.callQueue.entity.save); - expect(schema.callQueue.entity.save.pres[0]).equal(preMiddleware); - expect(schema.callQueue.entity.save.pres[1]).equal(preMiddleware); - }); + expect(fn).to.throw(Error); + }); + }); - it('should add post hooks to callQueue', () => { - const postMiddleware = () => { }; - const schema = new Schema({}); - schema.callQueue = { model: {}, entity: {} }; + describe('callQueue', () => { + it('should add pre hooks to callQueue', () => { + const preMiddleware = () => {}; + const schema = new Schema({}); + schema.__callQueue = { model: {}, entity: {} }; - schema.post('save', postMiddleware); + schema.pre('save', preMiddleware); + schema.pre('save', preMiddleware); // we add 2 so we test both cases L140 - assert.isDefined(schema.callQueue.entity.save); - expect(schema.callQueue.entity.save.post[0]).equal(postMiddleware); - }); + assert.isDefined(schema.__callQueue.entity.save); + expect(schema.__callQueue.entity.save.pres[0]).equal(preMiddleware); + expect(schema.__callQueue.entity.save.pres[1]).equal(preMiddleware); }); - describe('virtual()', () => { - it('should create new VirtualType', () => { - const schema = new Schema({}); - const fn = () => {}; - schema.virtual('fullname', fn); + it('should add post hooks to callQueue', () => { + const postMiddleware = () => {}; + const schema = new Schema({}); + schema.__callQueue = { model: {}, entity: {} }; - expect(schema.virtuals.fullname.constructor.name).equal('VirtualType'); - }); + schema.post('save', postMiddleware); - it('should set the scope on the entityData', () => { - const schema = new Schema({ id: {} }); - schema.virtual('fullname').get(virtualFunc); - const Model = gstore.model('VirtualTest', schema); - const entity = new Model({ id: 123 }); + assert.isDefined(schema.__callQueue.entity.save); + expect(schema.__callQueue.entity.save.post[0]).equal(postMiddleware); + }); + }); - entity.plain({ virtuals: true }); + describe('virtual()', () => { + it('should create new VirtualType', () => { + const schema = new Schema({}); + const fn = () => {}; + schema.virtual('fullname', fn); - function virtualFunc() { - expect(this).deep.equal(entity.entityData); - } - }); + expect(schema.__virtuals.fullname.constructor.name).equal('VirtualType'); }); - it('add shortCut queries settings', () => { - const schema = new Schema({}); - const listQuerySettings = { limit: 10, filters: [] }; + it('should set the scope on the entityData', () => { + const schema = new Schema({ id: {} }); + schema.virtual('fullname').get(virtualFunc); + const Model = gstore.model('VirtualTest', schema); + const entity = new Model({ id: 123 }); - schema.queries('list', listQuerySettings); + entity.plain({ virtuals: true }); - assert.isDefined(schema.shortcutQueries.list); - expect(schema.shortcutQueries.list).to.equal(listQuerySettings); + function virtualFunc() { + expect(this).deep.equal(entity.entityData); + } + }); + }); + + it('add shortCut queries settings', () => { + const schema = new Schema({}); + const listQuerySettings = { limit: 10, filters: [] }; + + schema.queries('list', listQuerySettings); + + assert.isDefined(schema.shortcutQueries.list); + expect(schema.shortcutQueries.list).to.equal(listQuerySettings); + }); + + describe('Joi', () => { + let schema; + + beforeEach(() => { + schema = new Schema( + { + name: { joi: Joi.string().required() }, + notJoi: { type: 'string' }, + }, + { + joi: true, + }, + ); }); - describe('Joi', () => { - let schema; - - beforeEach(() => { - schema = new Schema({ - name: { joi: Joi.string().required() }, - notJoi: { type: 'string' }, - }, { - joi: true, - }); - }); - - it('should build Joi schema', () => { - assert.isDefined(schema._joi); - }); + it('should build Joi schema', () => { + assert.isDefined(schema.joiSchema); }); + }); }); diff --git a/test/serializers/datastore-test.js b/test/serializers/datastore-test.js index 800e4dd..9cd5090 100644 --- a/test/serializers/datastore-test.js +++ b/test/serializers/datastore-test.js @@ -3,11 +3,11 @@ const chai = require('chai'); const Joi = require('@hapi/joi'); -const { Gstore } = require('../../lib'); +const { Gstore, QUERIES_FORMATS } = require('../../lib'); const ds = require('../mocks/datastore')({ - namespace: 'com.mydomain', + namespace: 'com.mydomain', }); -const datastoreSerializer = require('../../lib/serializer').Datastore; +const { datastoreSerializer } = require('../../lib/serializers'); const gstore = new Gstore(); const { Schema } = gstore; @@ -16,161 +16,155 @@ const { expect, assert } = chai; gstore.connect(ds); describe('Datastore serializer', () => { - let ModelInstance; + let Model; + + beforeEach(() => { + gstore.models = {}; + gstore.modelSchemas = {}; + }); + + describe('should convert data FROM Datastore format', () => { + let datastoreMock; + + const key = ds.key(['BlogPost', 1234]); + + let data; beforeEach(() => { - gstore.models = {}; - gstore.modelSchemas = {}; + const schema = new Schema({ + name: { type: String }, + email: { type: String, read: false }, + createdOn: { type: Date }, + }); + Model = gstore.model('Blog', schema, {}); + + data = { + name: 'John', + lastname: 'Snow', + email: 'john@snow.com', + createdOn: '2017-12-25', + }; + + datastoreMock = data; + datastoreMock[Model.gstore.ds.KEY] = key; }); - describe('should convert data FROM Datastore format', () => { - let datastoreMock; + it('and add Symbol("KEY") id to entity', () => { + const serialized = datastoreSerializer.fromDatastore(datastoreMock, Model); - const key = ds.key(['BlogPost', 1234]); + // expect(serialized).equal = datastoreMock; + expect(serialized.id).equal(key.id); + assert.isUndefined(serialized.email); + }); - let data; + it('accepting "readAll" param', () => { + const serialized = datastoreSerializer.fromDatastore(datastoreMock, Model, { readAll: true }); - beforeEach(() => { - const schema = new Schema({ - name: { type: 'string' }, - email: { type: 'string', read: false }, - createdOn: { type: 'datetime' }, - }); - ModelInstance = gstore.model('Blog', schema, {}); + assert.isDefined(serialized.email); + }); - data = { - name: 'John', - lastname: 'Snow', - email: 'john@snow.com', - createdOn: '2017-12-25', - }; + it('accepting "showKey" param', () => { + const serialized = datastoreSerializer.fromDatastore(datastoreMock, Model, { showKey: true }); - datastoreMock = data; - datastoreMock[ModelInstance.gstore.ds.KEY] = key; - }); + expect(serialized.__key).equal(key); + }); - it('and add Symbol("KEY") id to entity', () => { - const serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock); + it('should convert to entity instances', () => { + const serialized = datastoreSerializer.fromDatastore(datastoreMock, Model, { format: QUERIES_FORMATS.ENTITY }); - // expect(serialized).equal = datastoreMock; - expect(serialized.id).equal(key.id); - assert.isUndefined(serialized.email); - }); + expect(serialized.__className).equal('Entity'); + }); - it('accepting "readAll" param', () => { - const serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock, { readAll: true }); + it('should convert Datetime prop to Date object if returned as number', () => { + const date = Date.now(); + datastoreMock.createdOn = date; - assert.isDefined(serialized.email); - }); + const serialized = datastoreSerializer.fromDatastore(datastoreMock, Model); - it('accepting "showKey" param', () => { - const serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock, { showKey: true }); + assert.isDefined(serialized.createdOn.getDate); + }); + }); - expect(serialized.__key).equal(key); - }); + describe('should convert data TO Datastore format', () => { + let entity; - it('should convert to entity instances', () => { - const serialized = datastoreSerializer - .fromDatastore - .call(ModelInstance, datastoreMock, { format: gstore.Queries.formats.ENTITY }); + beforeEach(() => { + const schema = new Schema({ + name: { type: String, excludeFromIndexes: true }, + lastname: { type: String }, + embedded: { type: Object, excludeFromIndexes: 'description' }, + array: { type: Array, excludeFromIndexes: true }, + array2: { type: Array, excludeFromIndexes: true, joi: Joi.array() }, + array3: { type: Array, excludeFromIndexes: true, optional: true }, + }); + Model = gstore.model('Serializer', schema); + + entity = new Model({ + name: 'John', + lastname: undefined, + embedded: { + description: 'Long string (...)', + }, + array2: [1, 2, 3], + }); + }); - expect(serialized.className).equal('Entity'); - }); + it('without passing non-indexed properties', () => { + const expected = { + name: 'John', + embedded: { + description: 'Long string (...)', + }, + array2: [1, 2, 3], + array: null, + }; + const { data, excludeLargeProperties } = datastoreSerializer.toDatastore(entity); + expect(data).to.deep.equal(expected); + expect(excludeLargeProperties).to.equal(false); + }); - it('should convert Datetime prop to Date object if returned as number', () => { - const date = Date.now(); - datastoreMock.createdOn = date; + it('not taking into account "undefined" variables', () => { + const { data } = datastoreSerializer.toDatastore(entity); + expect({}.hasOwnProperty.call(data, 'lastname')).equal(false); + }); + + it('and set excludeFromIndexes properties', () => { + const { excludeFromIndexes } = datastoreSerializer.toDatastore(entity); + expect(excludeFromIndexes).to.deep.equal(['name', 'embedded.description', 'array2[]', 'array2[].*']); + }); - const serialized = datastoreSerializer - .fromDatastore - .call(ModelInstance, datastoreMock); + it('and set excludeLargeProperties flag', () => { + const schema = new Schema({ name: String }, { excludeLargeProperties: true }); + Model = gstore.model('Serializer-auto-unindex', schema); + entity = new Model({ name: 'John' }); - assert.isDefined(serialized.createdOn.getDate); - }); + const { excludeLargeProperties } = datastoreSerializer.toDatastore(entity); + expect(excludeLargeProperties).equal(true); }); - describe('should convert data TO Datastore format', () => { - let entity; - - beforeEach(() => { - const schema = new Schema({ - name: { type: String, excludeFromIndexes: true }, - lastname: { type: String }, - embedded: { type: Object, excludeFromIndexes: 'description' }, - array: { type: Array, excludeFromIndexes: true }, - array2: { type: Array, excludeFromIndexes: true, joi: Joi.array() }, - array3: { type: Array, excludeFromIndexes: true, optional: true }, - }); - ModelInstance = gstore.model('Serializer', schema); - - entity = new ModelInstance({ - name: 'John', - lastname: undefined, - embedded: { - description: 'Long string (...)', - }, - array2: [1, 2, 3], - }); - }); - - it('without passing non-indexed properties', () => { - const expected = { - name: 'John', - embedded: { - description: 'Long string (...)', - }, - array2: [1, 2, 3], - array: null, - }; - const { data, excludeLargeProperties } = datastoreSerializer.toDatastore(entity); - expect(data).to.deep.equal(expected); - expect(excludeLargeProperties).to.equal(false); - }); - - it('not taking into account "undefined" variables', () => { - const { data } = datastoreSerializer.toDatastore(entity); - expect({}.hasOwnProperty.call(data, 'lastname')).equal(false); - }); - - it('and set excludeFromIndexes properties', () => { - const { excludeFromIndexes } = datastoreSerializer.toDatastore(entity); - expect(excludeFromIndexes).to.deep.equal(['name', 'embedded.description', 'array2[]', 'array2[].*']); - }); - - it('and set excludeLargeProperties flag', () => { - const schema = new Schema({ name: String }, { excludeLargeProperties: true }); - const Model = gstore.model('Serializer-auto-unindex', schema); - entity = new Model({ name: 'John' }); - - const { excludeLargeProperties } = datastoreSerializer.toDatastore(entity); - expect(excludeLargeProperties).equal(true); - }); - - it('should set all excludeFromIndexes on all properties of object', () => { - const schema = new Schema({ - embedded: { type: Object, excludeFromIndexes: true }, - embedded2: { joi: Joi.object(), excludeFromIndexes: true }, - embedded3: { joi: Joi.object(), excludeFromIndexes: true }, - }); - ModelInstance = gstore.model('Serializer2', schema); - - entity = new ModelInstance({ - embedded: { - prop1: 123, - prop2: 123, - prop3: 123, - }, - embedded2: { - prop1: 123, - prop2: 123, - prop3: 123, - }, - }); - - const serialized = datastoreSerializer.toDatastore(entity); - expect(serialized.excludeFromIndexes).to.deep.equal([ - 'embedded', 'embedded.*', 'embedded2', 'embedded2.*', - ]); - }); + it('should set all excludeFromIndexes on all properties of object', () => { + const schema = new Schema({ + embedded: { type: Object, excludeFromIndexes: true }, + embedded2: { joi: Joi.object(), excludeFromIndexes: true }, + embedded3: { joi: Joi.object(), excludeFromIndexes: true }, + }); + Model = gstore.model('Serializer2', schema); + + entity = new Model({ + embedded: { + prop1: 123, + prop2: 123, + prop3: 123, + }, + embedded2: { + prop1: 123, + prop2: 123, + prop3: 123, + }, + }); + + const serialized = datastoreSerializer.toDatastore(entity); + expect(serialized.excludeFromIndexes).to.deep.equal(['embedded', 'embedded.*', 'embedded2', 'embedded2.*']); }); + }); }); diff --git a/test/virtualType-test.js b/test/virtualType-test.js index 600360b..7efd355 100644 --- a/test/virtualType-test.js +++ b/test/virtualType-test.js @@ -1,93 +1,93 @@ 'use strict'; const chai = require('chai'); -const VirtualType = require('../lib/virtualType'); +const { default: VirtualType } = require('../lib/virtualType'); const { expect } = chai; describe('VirtualType', () => { - it('should add function to getter array', () => { - const virtualType = new VirtualType('fullname'); + it('should add function to getter array', () => { + const virtualType = new VirtualType('fullname'); - virtualType.get(() => { }); + virtualType.get(() => {}); - expect(virtualType.getter).not.equal(null); - }); - - it('should throw error if not passing a function', () => { - const virtualType = new VirtualType('fullname'); + expect(virtualType.getter).not.equal(null); + }); - const fn = () => { - virtualType.get('string'); - }; + it('should throw error if not passing a function', () => { + const virtualType = new VirtualType('fullname'); - expect(fn).throw(Error); - }); + const fn = () => { + virtualType.get('string'); + }; - it('should add function to setter array', () => { - const virtualType = new VirtualType('fullname'); + expect(fn).throw(Error); + }); - virtualType.set(() => { }); - - expect(virtualType.setter).not.equal(null); - }); + it('should add function to setter array', () => { + const virtualType = new VirtualType('fullname'); - it('should throw error if not passing a function', () => { - const virtualType = new VirtualType('fullname'); + virtualType.set(() => {}); - const fn = () => { - virtualType.set('string'); - }; + expect(virtualType.setter).not.equal(null); + }); - expect(fn).throw(Error); - }); + it('should throw error if not passing a function', () => { + const virtualType = new VirtualType('fullname'); - it('should applyGetter with scope', () => { - const virtualType = new VirtualType('fullname'); + const fn = () => { + virtualType.set('string'); + }; - virtualType.get(function getName() { - return `${this.name} ${this.lastname}`; - }); + expect(fn).throw(Error); + }); - const entityData = { - name: 'John', - lastname: 'Snow', - }; + it('should applyGetter with scope', () => { + const virtualType = new VirtualType('fullname'); - virtualType.applyGetters(entityData); - expect(entityData.fullname).equal('John Snow'); + virtualType.get(function getName() { + return `${this.name} ${this.lastname}`; }); - it('should return null if no getter', () => { - const virtualType = new VirtualType('fullname'); + const entityData = { + name: 'John', + lastname: 'Snow', + }; - const entityData = {}; + virtualType.applyGetters(entityData); + expect(entityData.fullname).equal('John Snow'); + }); - const v = virtualType.applyGetters(entityData); - expect(v).equal(null); - }); + it('should return null if no getter', () => { + const virtualType = new VirtualType('fullname'); - it('should applySetter with scope', () => { - const virtualType = new VirtualType('fullname'); + const entityData = {}; - virtualType.set(function setName(name) { - const split = name.split(' '); - [this.firstname, this.lastname] = split; - }); + const v = virtualType.applyGetters(entityData); + expect(v).equal(null); + }); - const entityData = {}; + it('should applySetter with scope', () => { + const virtualType = new VirtualType('fullname'); - virtualType.applySetters('John Snow', entityData); - expect(entityData.firstname).equal('John'); - expect(entityData.lastname).equal('Snow'); + virtualType.set(function setName(name) { + const split = name.split(' '); + [this.firstname, this.lastname] = split; }); - it('should not do anything if no setter', () => { - const virtualType = new VirtualType('fullname'); + const entityData = {}; - const entityData = {}; + virtualType.applySetters('John Snow', entityData); + expect(entityData.firstname).equal('John'); + expect(entityData.lastname).equal('Snow'); + }); - virtualType.applySetters('John Snow', entityData); - expect(Object.keys(entityData).length).equal(0); - }); + it('should not do anything if no setter', () => { + const virtualType = new VirtualType('fullname'); + + const entityData = {}; + + virtualType.applySetters('John Snow', entityData); + expect(Object.keys(entityData).length).equal(0); + }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..233078f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "target": "es6", + "module": "commonjs", + "allowSyntheticDefaultImports": true, + "lib": ["esnext"], + "strict": true, + "removeComments": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": false, + "rootDir": ".", + "outDir": "lib", + "types": [ + "node", + "jest" + ], + "allowJs": false + }, + "include": [ + "src", + "typings", + "src/.eslintrc.js" + ] +} diff --git a/typings/nsql-cache-datastore/index.d.ts b/typings/nsql-cache-datastore/index.d.ts new file mode 100644 index 0000000..d8d07fb --- /dev/null +++ b/typings/nsql-cache-datastore/index.d.ts @@ -0,0 +1,6 @@ +declare module 'nsql-cache-datastore' { + function factory(ds?: any): { + keyToString: (key: any) => string + } + export default factory; +} diff --git a/typings/nsql-cache/index.d.ts b/typings/nsql-cache/index.d.ts new file mode 100644 index 0000000..d2ccc17 --- /dev/null +++ b/typings/nsql-cache/index.d.ts @@ -0,0 +1,117 @@ +declare module 'nsql-cache' { + import { entity } from '@google-cloud/datastore/build/src/entity'; + import { Query } from "@google-cloud/datastore/build/src/query"; + + export interface NsqlCacheConfig { + ttl?: { + keys?: number, + queries?: number, + memory?: { + keys: number, + queries: number + }, + redis?: { + keys: number, + queries: number + }, + [key: string]: { + keys: number, + queries: number + } | number | undefined, + + }, + cachePrefix?: { + keys?: string; + queries?: string; + }, + hashCacheKeys?: boolean, + wrapClient?: boolean, + global?: boolean; +} + + /** + * gstore-cache Instance + * + * @class Cache + */ + class NsqlCache { + constructor(settings: { db: any, stores: any[], config: NsqlCacheConfig }) + + public config: any; + + keys: { + read( + keys: entity.Key | entity.Key[], + options?: { ttl?: number | { [propName: string]: number } }, + fetchHandler?: (keys: entity.Key | entity.Key[]) => Promise + ): Promise; + get(key: entity.Key): Promise; + mget(...keys: entity.Key[]): Promise; + set(key: entity.Key, data: any, options?: { ttl: number | { [propName: string]: number } }): Promise; + mset(...args: any[]): Promise; + del(...keys: entity.Key[]): Promise; + }; + + queries: { + read( + query: Omit, + options?: { ttl?: number | { [propName: string]: number } }, + fetchHandler?: (query: Query) => Promise + ): Promise; + get(query: Query): Promise; + mget(...queries: Query[]): Promise; + set( + query: Query, + data: any, + options?: { ttl: number | { [propName: string]: number } } + ): Promise; + mset(...args: any[]): Promise; + kset(key: string, data: any, entityKinds: string | string[], options?: { ttl: number }): Promise; + clearQueriesByKind(entityKinds: string | string[]): Promise; + del(...queries: Query[]): Promise; + }; + + /** + * Retrieve an element from the cache + * + * @param {string} key The cache key + */ + get(key: string): Promise; + + /** + * Retrieve multiple elements from the cache + * + * @param {...string[]} keys Unlimited number of keys + */ + mget(...keys: string[]): Promise; + + /** + * Add an element to the cache + * + * @param {string} key The cache key + * @param {*} value The data to save in the cache + */ + set(key: string, value: any): Promise; + + /** + * Add multiple elements into the cache + * + * @param {...any[]} args Key Value pairs (key1, data1, key2, data2...) + */ + mset(...args: any[]): Promise; + + /** + * Remove one or multiple elements from the cache + * + * @param {string[]} keys The keys to remove + */ + del(keys: string[]): Promise; + + /** + * Clear the cache + */ + reset(): Promise; + } + + export default NsqlCache; +} diff --git a/typings/optional/index.d.ts b/typings/optional/index.d.ts new file mode 100644 index 0000000..93e73bf --- /dev/null +++ b/typings/optional/index.d.ts @@ -0,0 +1,4 @@ +declare module 'optional' { + const optional: (package: string) => any; + export default optional; +} diff --git a/typings/promised-hooks/index.d.ts b/typings/promised-hooks/index.d.ts new file mode 100644 index 0000000..e77b32c --- /dev/null +++ b/typings/promised-hooks/index.d.ts @@ -0,0 +1,12 @@ +declare module 'promised-hooks' { + interface Hooks { + wrap: (obj: any) => any; + ERRORS: symbol; + } + + function wrap(obj: any): any + + const hooks: Hooks; + + export default hooks +} diff --git a/yarn.lock b/yarn.lock index 6183334..ea978c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,26 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/core@^7.1.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" + integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helpers" "^7.6.0" + "@babel/parser" "^7.6.0" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.4.0", "@babel/generator@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.0.tgz#e2c21efbfd3293ad819a2359b448f002bfdfda56" @@ -43,6 +63,11 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + "@babel/helper-split-export-declaration@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" @@ -50,6 +75,15 @@ dependencies: "@babel/types" "^7.4.4" +"@babel/helpers@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.0.tgz#21961d16c6a3c3ab597325c34c465c0887d31c6e" + integrity sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ== + dependencies: + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -59,6 +93,11 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/parser@^7.1.0", "@babel/parser@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" + integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== + "@babel/parser@^7.2.2": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.1.tgz#8f4ffd45f779e6132780835ffa7a215fa0b2d181" @@ -69,10 +108,12 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== -"@babel/parser@^7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" - integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" "@babel/template@^7.1.0": version "7.2.2" @@ -83,7 +124,7 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/template@^7.4.0": +"@babel/template@^7.4.0", "@babel/template@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== @@ -92,7 +133,7 @@ "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" -"@babel/traverse@^7.4.3": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516" integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ== @@ -116,7 +157,7 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0": +"@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0": version "7.6.1" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== @@ -125,6 +166,14 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + "@commitlint/execute-rule@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-8.1.0.tgz#e8386bd0836b3dcdd41ebb9d5904bbeb447e4715" @@ -235,6 +284,154 @@ dependencies: "@hapi/hoek" "6.x.x" +"@jest/console@^24.7.1", "@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -324,6 +521,46 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@types/arrify@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/arrify/-/arrify-2.0.1.tgz#d50abf903b1019b08c2ee21cf3ded40084201512" + integrity sha512-eL0bkcwbr+BXp/PPat6+z8C11Hf6+CcB8aE1lIk+Nwvj7uDA3NUmEUgfKLYqvvSuVmeldmaWvo6+s7q9tC9xUQ== + dependencies: + arrify "*" + +"@types/babel__core@^7.1.0": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" + integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" + integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + dependencies: + "@babel/types" "^7.3.0" + "@types/duplexify@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8" @@ -331,6 +568,77 @@ dependencies: "@types/node" "*" +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + +"@types/extend@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/extend/-/extend-3.0.1.tgz#923dc2d707d944382433e01d6cc0c69030ab2c75" + integrity sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw== + +"@types/is@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@types/is/-/is-0.0.21.tgz#1f4b0afe428991564abbd890844eba2458bc83e6" + integrity sha512-j2v73mj4I47lOaA8r+5tknsgsqlRPDN224sS0Y7ORzlx3AXxqL6RhK/BRhEMP98RJIVW7WTnXZ9ZqQSNdWWuwg== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest-diff@*": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" + integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + +"@types/jest@^24.0.18": + version "24.0.18" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498" + integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ== + dependencies: + "@types/jest-diff" "*" + +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + +"@types/lodash.get@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" + integrity sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash.set@^4.3.6": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.6.tgz#33e635c2323f855359225df6a5c8c6f1f1908264" + integrity sha512-ZeGDDlnRYTvS31Laij0RsSaguIUSBTYIlJFKL3vm3T2OAZAQj2YpSvVWJc0WiG4jqg9fGX6PAPGvDqBcHfSgFg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.138" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.138.tgz#34f52640d7358230308344e579c15b378d91989e" + integrity sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg== + "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" @@ -346,6 +654,68 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/validator@^10.11.3": + version "10.11.3" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25" + integrity sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w== + +"@types/yargs-parser@*": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== + +"@types/yargs@^13.0.0": + version "13.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.2.tgz#a64674fc0149574ecd90ba746e932b5a5f7b3653" + integrity sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.2.0.tgz#cba8caa6ad8df544c46bca674125a31af8c9ac2f" + integrity sha512-rOodtI+IvaO8USa6ValYOrdWm9eQBgqwsY+B0PPiB+aSiK6p6Z4l9jLn/jI3z3WM4mkABAhKIqvGIBl0AFRaLQ== + dependencies: + "@typescript-eslint/experimental-utils" "2.2.0" + eslint-utils "^1.4.2" + functional-red-black-tree "^1.0.1" + regexpp "^2.0.1" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.2.0.tgz#31d855fbc425168ecf56960c777aacfcca391cff" + integrity sha512-IMhbewFs27Frd/ICHBRfIcsUCK213B8MsEUqvKFK14SDPjPR5JF6jgOGPlroybFTrGWpMvN5tMZdXAf+xcmxsA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.2.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.2.0.tgz#3cd758ed85ae9be06667beb61bbdf8060f274fb7" + integrity sha512-0mf893kj9L65O5sA7wP6EoYvTybefuRFavUNhT7w9kjhkdZodoViwVS+k3D+ZxKhvtL7xGtP/y/cNMJX9S8W4A== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.2.0" + "@typescript-eslint/typescript-estree" "2.2.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.2.0.tgz#1e2aad5ed573f9f7a8e3261eb79404264c4fc57f" + integrity sha512-9/6x23A3HwWWRjEQbuR24on5XIfVmV96cDpGR9671eJv1ebFKHj2sGVVAwkAVXR2UNuhY1NeKS2QMv5P8kQb2Q== + dependencies: + glob "^7.1.4" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -354,6 +724,11 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" +abab@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.1.tgz#3fa17797032b71410ec372e11668f4b4ffc86a82" + integrity sha512-1zSbbCuoIjafKZ3mblY5ikvAb0ODUbqBnFuUb7f6uLeQhhGJ0vEV4ntmtxKLT2WgXCO94E07BjunsIw1jOMPZw== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -366,11 +741,34 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +acorn-globals@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + acorn-jsx@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== +acorn-walk@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^5.5.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" + integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== + acorn@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a" @@ -408,7 +806,7 @@ ansi-colors@3.2.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== -ansi-escapes@^3.2.0: +ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -433,11 +831,6 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -445,13 +838,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" + micromatch "^3.1.4" + normalize-path "^2.1.1" append-transform@^1.0.0: version "1.0.0" @@ -485,19 +878,12 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-flatten@^1.0.1, arr-flatten@^1.1.0: +arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== @@ -507,6 +893,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -535,26 +926,21 @@ array-includes@^3.0.3: define-properties "^1.1.2" es-abstract "^1.7.0" -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +arrify@*, arrify@^2.0.0, arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + arrify@1.0.1, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -arrify@^2.0.0, arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -598,10 +984,10 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== -async-each@^1.0.0: +async-limiter@~1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== async@1.5.2, async@^1.4.0: version "1.5.2" @@ -628,432 +1014,45 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -babel-cli@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" - integrity sha1-UCq1SHTX24itALiHoGODzgPQAvE= - dependencies: - babel-core "^6.26.0" - babel-polyfill "^6.26.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - commander "^2.11.0" - convert-source-map "^1.5.0" - fs-readdir-recursive "^1.0.0" - glob "^7.1.2" - lodash "^4.17.4" - output-file-sync "^1.1.2" - path-is-absolute "^1.0.1" - slash "^1.0.0" - source-map "^0.5.6" - v8flags "^2.1.1" - optionalDependencies: - chokidar "^1.6.1" - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" -babel-plugin-transform-es2015-classes@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.2" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-regenerator@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== dependencies: - regenerator-transform "^0.10.0" + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" + "@types/babel__traverse" "^7.0.6" -babel-polyfill@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" - integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= - dependencies: - babel-runtime "^6.26.0" - core-js "^2.5.0" - regenerator-runtime "^0.10.5" - -babel-preset-es2015@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" - integrity sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk= - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.24.1" - babel-plugin-transform-es2015-classes "^6.24.1" - babel-plugin-transform-es2015-computed-properties "^6.24.1" - babel-plugin-transform-es2015-destructuring "^6.22.0" - babel-plugin-transform-es2015-duplicate-keys "^6.24.1" - babel-plugin-transform-es2015-for-of "^6.22.0" - babel-plugin-transform-es2015-function-name "^6.24.1" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-plugin-transform-es2015-modules-systemjs "^6.24.1" - babel-plugin-transform-es2015-modules-umd "^6.24.1" - babel-plugin-transform-es2015-object-super "^6.24.1" - babel-plugin-transform-es2015-parameters "^6.24.1" - babel-plugin-transform-es2015-shorthand-properties "^6.24.1" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.24.1" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.22.0" - babel-plugin-transform-es2015-unicode-regex "^6.24.1" - babel-plugin-transform-regenerator "^6.24.1" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: +babel-runtime@^6.23.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -1061,47 +1060,6 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runti core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -1142,11 +1100,6 @@ bignumber.js@^7.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== -binary-extensions@^1.0.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.0.tgz#9523e001306a32444b907423f1de2164222f6ab1" - integrity sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw== - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1160,15 +1113,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -1190,6 +1134,18 @@ brorand@^1.0.1: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -1254,6 +1210,13 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +bser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" + integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== + dependencies: + node-int64 "^0.4.0" + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -1389,6 +1352,18 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1415,17 +1390,6 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4. escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chance@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.0.tgz#4206b8ae8679275afd7db6379abc23f103ab8488" @@ -1441,27 +1405,16 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@^1.6.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - integrity sha1-eY5ol3gVHIB2tLNg5e3SjNortGg= - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -1519,6 +1472,11 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -1551,11 +1509,6 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -1810,7 +1763,7 @@ conventional-recommended-bump@6.0.0: meow "^4.0.0" q "^1.5.1" -convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0: +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== @@ -1822,7 +1775,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@^2.4.0, core-js@^2.5.0: +core-js@^2.4.0: version "2.6.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.3.tgz#4b70938bdffdaf64931e66e2db158f0892289c49" integrity sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ== @@ -1932,6 +1885,18 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + dependencies: + cssom "0.3.x" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1982,6 +1947,15 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + dataloader@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" @@ -2122,13 +2096,6 @@ detect-indent@6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -2139,6 +2106,16 @@ detect-newline@3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.0.0.tgz#8ae477c089e51872c264531cd6547719c0b86b2f" integrity sha512-JAP22dVPAqvhdRFFxK1G5GViIokyUn0UWXRNW0ztK96fsqi9cuM8w8ESbSk+T2w5OVorcMcL6m7yUg1RrX+2CA== +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -2173,6 +2150,13 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" @@ -2313,11 +2297,23 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escodegen@^1.9.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" + integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-airbnb-base@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz#8a7bcb9643d13c55df4dd7444f138bf4efa61e17" @@ -2327,6 +2323,13 @@ eslint-config-airbnb-base@^14.0.0: object.assign "^4.1.0" object.entries "^1.1.0" +eslint-config-prettier@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3" + integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A== + dependencies: + get-stdin "^6.0.0" + eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" @@ -2383,6 +2386,13 @@ eslint-plugin-mocha@^6.1.0: dependencies: ramda "^0.26.1" +eslint-plugin-prettier@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d" + integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA== + dependencies: + prettier-linter-helpers "^1.0.0" + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -2460,6 +2470,11 @@ espree@^6.1.1: acorn-jsx "^5.0.2" eslint-visitor-keys "^1.1.0" +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -2484,6 +2499,11 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2507,6 +2527,11 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -2520,12 +2545,10 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= expand-brackets@^2.1.4: version "2.1.4" @@ -2540,13 +2563,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -2554,6 +2570,18 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -2583,13 +2611,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -2619,6 +2640,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -2634,6 +2660,13 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ== +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + figures@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9" @@ -2655,22 +2688,6 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -2764,18 +2781,11 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== -for-in@^1.0.1, for-in@^1.0.2: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - foreground-child@^1.5.6: version "1.5.6" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" @@ -2828,23 +2838,18 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.2.1" -fs-readdir-recursive@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" - integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + nan "^2.12.1" + node-pre-gyp "^0.12.0" function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" @@ -2919,6 +2924,11 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2980,21 +2990,6 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - glob-parent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" @@ -3014,7 +3009,7 @@ glob@7.1.3, glob@^7.0.0, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.4: +glob@7.1.4, glob@^7.1.1, glob@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -3058,11 +3053,6 @@ globals@^11.1.0, globals@^11.7.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.10.0.tgz#1e09776dffda5e01816b3bb4077c8b59c24eaa50" integrity sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ== -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - google-auth-library@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-5.2.1.tgz#3df7cb4186a24355184551837b24012cb1b31013" @@ -3103,7 +3093,7 @@ google-p12-pem@^2.0.0: dependencies: node-forge "^0.9.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== @@ -3118,6 +3108,11 @@ growl@1.10.5, "growl@~> 1.10.0": resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + gtoken@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-4.0.0.tgz#42b63a935a03a61eedf0ec14f74f6875bad627bd" @@ -3152,13 +3147,6 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3249,14 +3237,6 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -3269,6 +3249,13 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -3291,7 +3278,7 @@ https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" -iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3331,6 +3318,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -3419,7 +3414,7 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -invariant@^2.2.2: +invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -3455,13 +3450,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -3484,6 +3472,13 @@ is-callable@^1.1.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -3526,18 +3521,6 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3550,11 +3533,6 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3579,12 +3557,10 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" @@ -3593,13 +3569,6 @@ is-glob@^4.0.0, is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -3607,11 +3576,6 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -3627,17 +3591,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= + isobject "^3.0.1" is-promise@^2.1.0: version "2.1.0" @@ -3690,6 +3644,11 @@ is-windows@^1.0.1, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + is@^3.2.0, is@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" @@ -3727,7 +3686,7 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@^2.0.5: +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== @@ -3739,7 +3698,7 @@ istanbul-lib-hook@^2.0.7: dependencies: append-transform "^1.0.0" -istanbul-lib-instrument@^3.3.0: +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== @@ -3752,7 +3711,7 @@ istanbul-lib-instrument@^3.3.0: istanbul-lib-coverage "^2.0.5" semver "^6.0.0" -istanbul-lib-report@^2.0.8: +istanbul-lib-report@^2.0.4, istanbul-lib-report@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== @@ -3761,7 +3720,7 @@ istanbul-lib-report@^2.0.8: make-dir "^2.1.0" supports-color "^6.1.0" -istanbul-lib-source-maps@^3.0.6: +istanbul-lib-source-maps@^3.0.1, istanbul-lib-source-maps@^3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== @@ -3772,23 +3731,372 @@ istanbul-lib-source-maps@^3.0.6: rimraf "^2.6.3" source-map "^0.6.1" -istanbul-reports@^2.2.4: +istanbul-reports@^2.2.4, istanbul-reports@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== dependencies: handlebars "^4.1.2" +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== + dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== + dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" + +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" + +jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" + integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== + dependencies: + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" + +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== + dependencies: + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== + dependencies: + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" + +jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== + dependencies: + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== + +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0, jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - js-yaml@3.13.1, js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" @@ -3802,21 +4110,43 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - json-bigint@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" @@ -3849,10 +4179,12 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" jsonfile@^4.0.0: version "4.0.0" @@ -3922,6 +4254,11 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -3941,6 +4278,16 @@ lcov-parse@^0.0.10: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" integrity sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM= +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -4048,6 +4395,11 @@ lodash.set@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + lodash.template@^4.0.2: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" @@ -4063,6 +4415,11 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@4.17.14: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" @@ -4151,6 +4508,13 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + map-age-cleaner@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -4180,11 +4544,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -4246,31 +4605,17 @@ merge-source-map@^1.1.0: dependencies: source-map "^0.6.1" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.0.4, micromatch@^3.1.10: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -4349,7 +4694,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -4448,10 +4793,10 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== nanomatch@^1.2.9: version "1.2.13" @@ -4538,6 +4883,11 @@ node-forge@^0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + "node-libs-browser@^1.0.0 || ^2.0.0": version "2.2.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77" @@ -4567,10 +4917,26 @@ node-forge@^0.9.0: util "^0.11.0" vm-browserify "0.0.4" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.4.2: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -4601,7 +4967,7 @@ normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.0, normalize-path@^2.0.1: +normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= @@ -4664,6 +5030,11 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +nwsapi@^2.0.7: + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + nyc@^14.1.1: version "14.1.1" resolved "https://registry.yarnpkg.com/nyc/-/nyc-14.1.1.tgz#151d64a6a9f9f5908a1b73233931e4a0a3075eeb" @@ -4764,14 +5135,6 @@ object.getownpropertydescriptors@^2.0.3: define-properties "^1.1.2" es-abstract "^1.5.1" -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -4816,7 +5179,7 @@ optional@^0.1.4: resolved "https://registry.yarnpkg.com/optional/-/optional-0.1.4.tgz#cdb1a9bedc737d2025f690ceeb50e049444fd5b3" integrity sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw== -optionator@^0.8.2: +optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= @@ -4854,7 +5217,7 @@ os-locale@^3.0.0, os-locale@^3.1.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -4867,20 +5230,18 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" - integrity sha1-0KM+7+YaIF+suQCS6CZZjVJFznY= - dependencies: - graceful-fs "^4.1.4" - mkdirp "^0.5.1" - object-assign "^4.1.0" - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -4933,6 +5294,11 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -4982,16 +5348,6 @@ parse-github-repo-url@^1.3.0: resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -5012,6 +5368,11 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -5039,7 +5400,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -5132,6 +5493,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -5146,6 +5514,11 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -5156,15 +5529,27 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" -private@^0.1.6, private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== +prettier@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" process-nextick-args@~2.0.0: version "2.0.0" @@ -5189,6 +5574,14 @@ promised-hooks@^3.1.0: arrify "^1.0.1" is "^3.2.0" +prompts@^2.0.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.2.1.tgz#f901dd2a2dfee080359c0e20059b24188d75ad35" + integrity sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.3" + protobufjs@^6.8.6, protobufjs@^6.8.8: version "6.8.8" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" @@ -5218,6 +5611,11 @@ psl@^1.1.24: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== +psl@^1.1.28: + version "1.4.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" + integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== + public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -5248,7 +5646,7 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -5283,15 +5681,6 @@ ramda@^0.26.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" @@ -5317,6 +5706,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.8.4: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" + integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -5407,14 +5801,12 @@ readable-stream@^3.0.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdirp@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" + util.promisify "^1.0.0" rechoir@^0.6.2: version "0.6.2" @@ -5463,37 +5855,11 @@ redis@^2.7.1: redis-commands "^1.2.0" redis-parser "^2.6.0" -regenerate@^1.2.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== - -regenerator-runtime@^0.10.5: - version "0.10.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" - integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= - regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -5507,27 +5873,6 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= - dependencies: - jsesc "~0.5.0" - release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -5545,7 +5890,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -5557,7 +5902,23 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@^2.86.0: +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.86.0, request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -5598,6 +5959,13 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" @@ -5633,6 +6001,11 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + resolve@^1.1.6, resolve@^1.5.0: version "1.10.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" @@ -5640,7 +6013,7 @@ resolve@^1.1.6, resolve@^1.5.0: dependencies: path-parse "^1.0.6" -resolve@^1.10.0, resolve@^1.11.0: +resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== @@ -5686,7 +6059,7 @@ rimraf@3.0.0: dependencies: glob "^7.1.3" -rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -5701,6 +6074,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -5732,6 +6110,21 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -5747,12 +6140,12 @@ secure-keys@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== -semver@6.3.0, semver@^6.0.0, semver@^6.1.2: +semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^5.6.0, semver@^5.7.0: +semver@^5.4.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -5816,6 +6209,11 @@ shelljs@0.7.6: interpret "^1.0.0" rechoir "^0.6.2" +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -5834,10 +6232,15 @@ sinon@^7.4.2: nise "^1.5.2" supports-color "^5.5.0" -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +sisteransi@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" + integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== slice-ansi@^2.1.0: version "2.1.0" @@ -5889,24 +6292,25 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== +source-map-support@^0.5.6: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: - source-map "^0.5.6" + buffer-from "^1.0.0" + source-map "^0.6.0" source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -5997,6 +6401,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + standard-version@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-7.0.0.tgz#4ce10ea5d20270ed4a32b22d15cce5fd1f1a5bbb" @@ -6025,6 +6434,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -6056,6 +6470,14 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -6201,11 +6623,6 @@ supports-color@6.0.0: dependencies: has-flag "^3.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -6220,6 +6637,11 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -6268,6 +6690,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + through2@^2.0.0, through2@^2.0.2: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -6302,16 +6729,16 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -6342,6 +6769,14 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +tough-cookie@^2.3.3, tough-cookie@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -6350,6 +6785,13 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -6370,11 +6812,23 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +tslib@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -6409,6 +6863,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" + integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== + uglify-js@^3.1.4: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -6465,16 +6924,19 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -user-home@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" - integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA= - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + util@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" @@ -6499,13 +6961,6 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== -v8flags@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" - integrity sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ= - dependencies: - user-home "^1.1.1" - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -6535,11 +6990,60 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + walkdir@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -6601,6 +7105,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" @@ -6617,6 +7130,18 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -6692,7 +7217,7 @@ yargs@13.2.2: y18n "^4.0.0" yargs-parser "^13.0.0" -yargs@13.3.0, yargs@^13.2.2: +yargs@13.3.0, yargs@^13.2.2, yargs@^13.3.0: version "13.3.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==