@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
190 lines (189 loc) • 8.18 kB
JavaScript
import { validateSplits } from '../inputValidation/splits';
import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE, WARN_LOWERCASE_FLAGSET, WARN_INVALID_FLAGSET, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants';
import { objectAssign } from '../lang/objectAssign';
import { find, uniq } from '../lang';
import { isConsumerMode } from './mode';
// Split filters metadata.
// Ordered according to their precedency when forming the filter query string: `&names=<values>&prefixes=<values>`
var FILTERS_METADATA = [
{
type: 'bySet',
maxLength: 50,
queryParam: 'sets='
},
{
type: 'byName',
maxLength: 400,
queryParam: 'names='
},
{
type: 'byPrefix',
maxLength: 50,
queryParam: 'prefixes='
}
];
var VALID_FLAGSET_REGEX = /^[a-z0-9][_a-z0-9]{0,49}$/;
var CAPITAL_LETTERS_REGEX = /[A-Z]/;
/**
* Validates that the given value is a valid filter type
*/
function validateFilterType(maybeFilterType) {
return FILTERS_METADATA.some(function (filterMetadata) { return filterMetadata.type === maybeFilterType; });
}
/**
* Validate, deduplicate and sort a given list of filter values.
*
* @param type - filter type string used for log messages
* @param values - list of values to validate, deduplicate and sort
* @param maxLength - max length allowed for the list of values
* @returns list of valid, unique and alphabetically sorted non-empty strings. The list is empty if `values` param is not a non-empty array or all its values are invalid.
*
* @throws Error if the sanitized list exceeds the length indicated by `maxLength`
*/
function validateSplitFilter(log, type, values, maxLength) {
// validate and remove invalid and duplicated values
var result = validateSplits(log, values, LOG_PREFIX_SETTINGS, type + " filter", type + " filter value");
if (result) {
if (type === 'bySet') {
result = sanitizeFlagSets(log, result, LOG_PREFIX_SETTINGS);
}
// check max length
if (result.length > maxLength)
throw new Error(maxLength + " unique values can be specified at most for '" + type + "' filter. You passed " + result.length + ". Please consider reducing the amount or using other filter.");
// sort values
result.sort();
}
return result || []; // returns empty array if `result` is `false`
}
/**
* Returns a string representing the URL encoded query component of /splitChanges URL.
*
* The possible formats of the query string are:
* - null: if all filters are empty
* - '&names=<comma-separated-values>': if only `byPrefix` filter is undefined
* - '&prefixes=<comma-separated-values>': if only `byName` filter is undefined
* - '&names=<comma-separated-values>&prefixes=<comma-separated-values>': if no one is undefined
*
* @param groupedFilters - object of filters. Each filter must be a list of valid, unique and ordered string values.
* @returns null or string with the `split filter query` component of the URL.
*/
function queryStringBuilder(groupedFilters) {
var queryParams = [];
FILTERS_METADATA.forEach(function (_a) {
var type = _a.type, queryParam = _a.queryParam;
var filter = groupedFilters[type];
if (filter.length > 0)
queryParams.push(queryParam + filter.map(function (value) { return encodeURIComponent(value); }).join(','));
});
return queryParams.length > 0 ? '&' + queryParams.join('&') : null;
}
/**
* Sanitizes set names list taking into account:
* - It should be lowercase
* - Must adhere the following regular expression `/^[a-z0-9][_a-z0-9]{0,49}$/` that means
* - must start with a letter or number
* - Be in lowercase
* - Be alphanumeric
* - have a max length of 50 characters
*
* @returns sanitized list of set names
*/
function sanitizeFlagSets(log, flagSets, method) {
var sanitizedSets = flagSets
.map(function (flagSet) {
if (CAPITAL_LETTERS_REGEX.test(flagSet)) {
log.warn(WARN_LOWERCASE_FLAGSET, [method, flagSet]);
flagSet = flagSet.toLowerCase();
}
return flagSet;
})
.filter(function (flagSet) {
if (!VALID_FLAGSET_REGEX.test(flagSet)) {
log.warn(WARN_INVALID_FLAGSET, [method, flagSet, VALID_FLAGSET_REGEX, flagSet]);
return false;
}
if (typeof flagSet !== 'string')
return false;
return true;
});
return uniq(sanitizedSets);
}
function configuredFilter(validFilters, filterType) {
return find(validFilters, function (filter) { return filter.type === filterType && filter.values.length > 0; });
}
/**
* Validates `splitFilters` configuration object and parses it into a query string for filtering splits on `/splitChanges` fetch.
*
* @param log - logger
* @param maybeSplitFilters - split filters configuration param provided by the user
* @param mode - settings mode
* @returns it returns an object with the following properties:
* - `validFilters`: the validated `splitFilters` configuration object defined by the user.
* - `queryString`: the parsed split filter query. it is null if all filters are invalid or all values in filters are invalid.
* - `groupedFilters`: list of values grouped by filter type.
*
* @throws Error if the some of the grouped list of values per filter exceeds the max allowed length
*/
export function validateSplitFilters(log, maybeSplitFilters, mode) {
// Validation result schema
var res = {
validFilters: [],
queryString: null,
groupedFilters: { bySet: [], byName: [], byPrefix: [] },
};
// do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE
if (!maybeSplitFilters)
return res;
// Warn depending on the mode
if (isConsumerMode(mode)) {
log.warn(WARN_SPLITS_FILTER_IGNORED);
return res;
}
// Check collection type
if (!Array.isArray(maybeSplitFilters) || maybeSplitFilters.length === 0) {
log.warn(WARN_SPLITS_FILTER_EMPTY);
return res;
}
// Validate filters and group their values by filter type inside `groupedFilters` object
res.validFilters = maybeSplitFilters.filter(function (filter, index) {
if (filter && validateFilterType(filter.type) && Array.isArray(filter.values)) {
res.groupedFilters[filter.type] = res.groupedFilters[filter.type].concat(filter.values);
return true;
}
else {
log.warn(WARN_SPLITS_FILTER_INVALID, [index]);
}
return false;
});
// By filter type, remove invalid and duplicated values and order them
FILTERS_METADATA.forEach(function (_a) {
var type = _a.type, maxLength = _a.maxLength;
if (res.groupedFilters[type].length > 0)
res.groupedFilters[type] = validateSplitFilter(log, type, res.groupedFilters[type], maxLength);
});
var setFilter = configuredFilter(res.validFilters, 'bySet');
// Clean all filters if set filter is present
if (setFilter) {
if (configuredFilter(res.validFilters, 'byName') || configuredFilter(res.validFilters, 'byPrefix'))
log.error(ERROR_SETS_FILTER_EXCLUSIVE);
objectAssign(res.groupedFilters, { byName: [], byPrefix: [] });
}
// build query string
res.queryString = queryStringBuilder(res.groupedFilters);
log.debug(SETTINGS_SPLITS_FILTER, [res.queryString]);
return res;
}
export function validateFlagSets(log, method, flagSets, flagSetsInConfig) {
var sets = validateSplits(log, flagSets, method, 'flag sets', 'flag set');
var toReturn = sets ? sanitizeFlagSets(log, sets, method) : [];
if (flagSetsInConfig.length > 0) {
toReturn = toReturn.filter(function (flagSet) {
if (flagSetsInConfig.indexOf(flagSet) > -1) {
return true;
}
log.warn(WARN_FLAGSET_NOT_CONFIGURED, [method, flagSet]);
return false;
});
}
return toReturn.length ? toReturn : false;
}