diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index 8e53a321..25c1e7f1 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -31,7 +31,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt const matcher = matcherFactory(log, matcherDto, storage); // Evaluator function. - return (key: string, attributes: SplitIO.Attributes, splitEvaluator: ISplitEvaluator) => { + return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => { const value = sanitizeValue(log, key, matcherDto, attributes); const result = value !== undefined && matcher ? matcher(value, splitEvaluator) : false; diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index ccab4db8..54078b5b 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -6,7 +6,7 @@ import { ILogger } from '../logger/types'; export interface IDependencyMatcherValue { key: SplitIO.SplitKey, - attributes: SplitIO.Attributes + attributes?: SplitIO.Attributes } export interface IMatcherDto { @@ -27,7 +27,7 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string } -export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes, storage: IStorageSync | IStorageAsync) => MaybeThenable +export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index 319e05e2..c564a68f 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types'; import { sanitize } from './sanitize'; import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(log: ILogger, key: string, attributeName: string | null, attributes: SplitIO.Attributes) { +function parseValue(log: ILogger, key: string, attributeName: string | null, attributes?: SplitIO.Attributes) { let value = undefined; if (attributeName) { if (attributes) { @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att /** * Defines value to be matched (key / attribute). */ -export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes: SplitIO.Attributes) { +export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { const attributeName = matcherDto.attribute; const valueToMatch = parseValue(log, key, attributeName, attributes); const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 6a36a59d..d12de8ed 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -41,7 +41,7 @@ function sanitizeBoolean(val: any): boolean | undefined { return undefined; } -function dependencyProcessor(sanitizedValue: string, attributes: SplitIO.Attributes): IDependencyMatcherValue { +function dependencyProcessor(sanitizedValue: string, attributes?: SplitIO.Attributes): IDependencyMatcherValue { return { key: sanitizedValue, attributes @@ -69,7 +69,7 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes: SplitIO.Attributes) { +export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes?: SplitIO.Attributes) { const processor = getProcessingFunction(matcherTypeID, dataType); let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; diff --git a/src/utils/inputValidation/attributes.ts b/src/utils/inputValidation/attributes.ts index 7a06ee90..e9824113 100644 --- a/src/utils/inputValidation/attributes.ts +++ b/src/utils/inputValidation/attributes.ts @@ -6,7 +6,7 @@ import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants'; export function validateAttributes(log: ILogger, maybeAttrs: any, method: string): SplitIO.Attributes | undefined | false { // Attributes are optional - if (isObject(maybeAttrs) || maybeAttrs == undefined) // eslint-disable-line eqeqeq + if (maybeAttrs == undefined || isObject(maybeAttrs)) // eslint-disable-line eqeqeq return maybeAttrs; log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'attributes']); @@ -23,5 +23,4 @@ export function validateAttributesDeep(log: ILogger, maybeAttributes: Record { // Object.create(null) creates an object with no prototype which may be tricky to handle. Filtering that out too. expect(isObject(Object.create(null))).toBe(false); // Should return false for anything that is not a plain object. + // validate on a different VM context + const ctx = vm.createContext({ isObject }); + vm.runInContext(` + var positives = + isObject({}) && + isObject({ a: true }) && + isObject(new Object()) && + isObject(Object.create({})) && + isObject(Object.create(Object.prototype)); + var negatives = + isObject([]) || + isObject(() => { }) || + isObject(null) || + isObject(undefined) || + isObject(new Promise(res => res())) || + isObject(Object.create(null)); + `, ctx); + expect(ctx.positives).toBe(true); + expect(ctx.negatives).toBe(false); }); test('LANG UTILS / uniqueId', () => { diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 94efa40d..bd7c7a51 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -150,35 +150,15 @@ export function isNaNNumber(val: any): boolean { return val !== val; } -function _isObject(o: any) { - return Object.prototype.toString.call(o) === '[object Object]'; -} - /** * Validates if a value is an object created by the Object constructor (plain object). - * Adapted from: - * - * is-plain-object - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. + * It checks `constructor.name` to avoid false negatives when validating values on a separate VM context, which has its own global built-ins. */ -export function isObject(o: any): boolean { - if (_isObject(o) === false) return false; - - // If has modified constructor - const ctor = o.constructor; - if (ctor === undefined) return false; // `Object.create(null)` - - // If has modified prototype - const prot = ctor.prototype; - if (_isObject(prot) === false) return false; - - // If constructor does not have an Object-specific method - if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) return false; - - // Most likely a plain Object - return true; +export function isObject(obj: any) { + return obj !== null && typeof obj === 'object' && ( + obj.constructor === Object || + (obj.constructor != null && obj.constructor.name === 'Object') + ); } /**