UNPKG

@splitsoftware/splitio-commons

Version:
190 lines (189 loc) 8.18 kB
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; }