Skip to content

Commit

Permalink
Validate getTreatments input, in the same way than React SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Dec 14, 2023
1 parent 97ec305 commit bb8e653
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 14 deletions.
16 changes: 7 additions & 9 deletions src/asyncActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Dispatch, Action } from 'redux';
import { IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ISplitFactoryBuilder } from './types';
import { splitReady, splitReadyWithEvaluations, splitReadyFromCache, splitReadyFromCacheWithEvaluations, splitTimedout, splitUpdate, splitUpdateWithEvaluations, splitDestroy, addTreatments } from './actions';
import { VERSION, ERROR_GETT_NO_INITSPLITSDK, ERROR_DESTROY_NO_INITSPLITSDK, getControlTreatmentsWithConfig } from './constants';
import { matching, getStatus } from './utils';
import { matching, getStatus, validateGetTreatmentsParams } from './utils';

/**
* Internal object SplitSdk. This object should not be accessed or
Expand Down Expand Up @@ -108,22 +108,20 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi
return () => { };
}

// Convert string feature flag name to a one item array.
if (typeof params.splitNames === 'string') {
params.splitNames = [params.splitNames];
}
params = validateGetTreatmentsParams(params);
const splitNames = params.splitNames as string[];

if (!splitSdk.isDetached) { // Split SDK running in Browser

const client = getClient(splitSdk, params.key);

// Register or unregister the current `getTreatments` action from being re-executed on SDK_UPDATE.
if (params.evalOnUpdate) {
params.splitNames.forEach((featureFlagName) => {
splitNames.forEach((featureFlagName) => {
client.evalOnUpdate[featureFlagName] = { ...params, splitNames: [featureFlagName] };
});
} else {
params.splitNames.forEach((featureFlagName) => {
splitNames.forEach((featureFlagName) => {
delete client.evalOnUpdate[featureFlagName];
});
}
Expand All @@ -147,14 +145,14 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi
} else {
// Otherwise, it adds control treatments to the store, without calling the SDK (no impressions sent)
// @TODO remove eventually to minimize state changes
return addTreatments(params.key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, getControlTreatmentsWithConfig(params.splitNames));
return addTreatments(params.key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, getControlTreatmentsWithConfig(splitNames));
}

} else { // Split SDK running in Node

// Evaluate Split and return redux action.
const client = splitSdk.factory.client();
const treatments = client.getTreatmentsWithConfig(params.key, params.splitNames, params.attributes);
const treatments = client.getTreatmentsWithConfig(params.key, splitNames, params.attributes);
return addTreatments(params.key, treatments);

}
Expand Down
10 changes: 5 additions & 5 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export const SPLIT_DESTROY = 'SPLIT_DESTROY';
export const ADD_TREATMENTS = 'ADD_TREATMENTS';

// Warning and error messages
export const ERROR_GETT_NO_INITSPLITSDK = '[Error] To use "getTreatments" the SDK must be first initialized with a "initSplitSdk" action';
export const ERROR_GETT_NO_INITSPLITSDK = '[ERROR] To use "getTreatments" the SDK must be first initialized with a "initSplitSdk" action';

export const ERROR_DESTROY_NO_INITSPLITSDK = '[Error] To use "destroySplitSdk" the SDK must be first initialized with a "initSplitSdk" action';
export const ERROR_DESTROY_NO_INITSPLITSDK = '[ERROR] To use "destroySplitSdk" the SDK must be first initialized with a "initSplitSdk" action';

export const ERROR_TRACK_NO_INITSPLITSDK = '[Error] To use "track" the SDK must be first initialized with an "initSplitSdk" action';
export const ERROR_TRACK_NO_INITSPLITSDK = '[ERROR] To use "track" the SDK must be first initialized with an "initSplitSdk" action';

export const ERROR_MANAGER_NO_INITSPLITSDK = '[Error] To use the manager, the SDK must be first initialized with an "initSplitSdk" action';
export const ERROR_MANAGER_NO_INITSPLITSDK = '[ERROR] To use the manager, the SDK must be first initialized with an "initSplitSdk" action';

export const ERROR_SELECTOR_NO_SPLITSTATE = '[Error] When using selectors, "splitState" value must be a proper splitio piece of state';
export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] When using selectors, "splitState" value must be a proper splitio piece of state';
77 changes: 77 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IGetTreatmentsParams } from './types';

/**
* Validates if a value is an object.
*/
Expand Down Expand Up @@ -29,3 +31,78 @@ export function getStatus(client: SplitIO.IClient): IClientStatus {
// @ts-expect-error, function exists but it is not part of JS SDK type definitions
return client.__getStatus();
}

/**
* Validates and sanitizes the parameters passed to the "getTreatments" action creator.
*
* @returns {IGetTreatmentsParams} The returned object is a copy of the passed one, with the "splitNames" property converted to an array of strings.
*/
export function validateGetTreatmentsParams(params: IGetTreatmentsParams) {
let { splitNames } = params;
if (typeof splitNames === 'string') splitNames = [splitNames];
if (!validateFeatureFlags(splitNames)) splitNames = [];

return {
...params,
splitNames: splitNames,
};
}

// The following input validation utils are based on the ones in the React SDK. They might be replaced by utils from the JS SDK in the future.

function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] {
if (Array.isArray(maybeFeatureFlags) && maybeFeatureFlags.length > 0) {
const validatedArray: string[] = [];
// Remove invalid values
maybeFeatureFlags.forEach((maybeFeatureFlag) => {
const featureFlagName = validateFeatureFlag(maybeFeatureFlag);
if (featureFlagName) validatedArray.push(featureFlagName);
});

// Strip off duplicated values if we have valid feature flag names then return
if (validatedArray.length) return uniq(validatedArray);
}

console.log(`[ERROR] ${listName} must be a non-empty array.`);
return false;
}

const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/;

function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag name'): false | string {
if (maybeFeatureFlag == undefined) {
console.log(`[ERROR] you passed a null or undefined ${item}, ${item} must be a non-empty string.`);
} else if (!isString(maybeFeatureFlag)) {
console.log(`[ERROR] you passed an invalid ${item}, ${item} must be a non-empty string.`);
} else {
if (TRIMMABLE_SPACES_REGEX.test(maybeFeatureFlag)) {
console.log(`[WARN] ${item} "${maybeFeatureFlag}" has extra whitespace, trimming.`);
maybeFeatureFlag = maybeFeatureFlag.trim();
}

if ((maybeFeatureFlag as string).length > 0) {
return maybeFeatureFlag as string;
} else {
console.log(`[ERROR] you passed an empty ${item}, ${item} must be a non-empty string.`);
}
}

return false;
}

/**
* Removes duplicate items on an array of strings.
*/
function uniq(arr: string[]): string[] {
const seen: Record<string, boolean> = {};
return arr.filter((item) => {
return Object.prototype.hasOwnProperty.call(seen, item) ? false : seen[item] = true;
});
}

/**
* Checks if a given value is a string.
*/
function isString(val: unknown): val is string {
return typeof val === 'string' || val instanceof String;
}
13 changes: 13 additions & 0 deletions types/utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IGetTreatmentsParams } from './types';
/**
* Validates if a value is an object.
*/
Expand All @@ -17,3 +18,15 @@ export interface IClientStatus {
isDestroyed: boolean;
}
export declare function getStatus(client: SplitIO.IClient): IClientStatus;
/**
* Validates and sanitizes the parameters passed to the "getTreatments" action creator.
*
* @returns {IGetTreatmentsParams} The returned object is a copy of the passed one, with the "splitNames" property converted to an array of strings.
*/
export declare function validateGetTreatmentsParams(params: IGetTreatmentsParams): {
splitNames: string[];
key?: import("@splitsoftware/splitio/types/splitio").SplitKey;
attributes?: import("@splitsoftware/splitio/types/splitio").Attributes;
evalOnUpdate?: boolean;
evalOnReadyFromCache?: boolean;
};

0 comments on commit bb8e653

Please sign in to comment.