unomi-sdk-node
Version:
Node module to interact with unomi.
700 lines (699 loc) • 40.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateSegmentCountObject = exports.isValidDateExpression = exports.reformatSegment = exports.formatProfileSegmentSubCondition = exports.formatProfilePropertySubCondition = exports.formatSubConditions = exports.validateRequiredProps = exports.validateSegmentObject = exports.validateSegmentsProperty = exports.validateMatchType = exports.validateComparisonOperatorUsage = exports.validatePropertyValue = exports.setExpectedValue = exports.checkExpectedValue = exports.formatPropertyName = exports.validatePropertyName = void 0;
const helpers_1 = require("../utils/helpers");
/**
* @function validatePropertyName
* @param {Array<any>} parameterValuesObject
*/
function validatePropertyName(parameterValuesObject) {
if (!parameterValuesObject.propertyName.startsWith("properties.")) { // check if "properties." is missing
parameterValuesObject.propertyName = "properties." + parameterValuesObject.propertyName; // add "properties." in front of property name
}
return parameterValuesObject; // return validated object
}
exports.validatePropertyName = validatePropertyName;
function formatPropertyName(subCondition) {
if (subCondition.parameterValues.propertyName.startsWith("properties.")) { // check if "properties." is not missing
subCondition.parameterValues.propertyName = subCondition.parameterValues.propertyName.replace("properties.", ""); // remove it from property name
}
return subCondition; // return formatted subcondition
}
exports.formatPropertyName = formatPropertyName;
function checkExpectedValue(propertyName) {
// note: CDP front-end sends all profile property values values as strings (except booleans), but some profile properties
// like "age" only work with integers so they have to be changed to the right type to be used in unomi segments
let noPrefixProperties = [
"age",
"firstVisit",
"lastVisit",
"nbOfVisits",
"previousVisit" //date
];
let words = propertyName.split("."); // split property name on dot
// eg: properties.capture_properties.event.eventData.target.size.do__height
// will become: [ 'properties', 'capture_properties', 'event', 'eventData', 'target', 'size', 'do__height' ]
let property = words[words.length - 1]; // get property name
let expectedType = "string"; // default expected type
if (noPrefixProperties.includes(property)) { // check if profile property is a default property of an unomi profile
switch (property) { // check which property it is
// these properties have integer values
case "age":
case "nbOfVisits":
expectedType = "number"; // expect integer/float value
break;
// these properties have date values
case "firstVisit":
case "lastVisit":
case "previousVisit":
expectedType = "string"; // expect string value
break;
}
}
else { // it's a profile property with a prefix
let prefix = property.substring(0, 2); // get prefix in property name
switch (prefix) { // check which prefix
case "te": // text (saved as string in unomi)
case "lt": // long text (saved as string in unomi)
case "da": // date (saved as string in unomi)
case "bo": // boolean (saved as string in unomi)
expectedType = "string"; // expect string value
break;
case "lo": // long (saved as integer in unomi)
case "do": // double (saved as float in unomi)
expectedType = "number"; // expect integer/float value
break;
}
}
return expectedType; // return expected type for property
}
exports.checkExpectedValue = checkExpectedValue;
function setExpectedValue(expectedType, propertyValue) {
if (typeof (propertyValue) === "object") { // check if array, since array is treated as object by typeof
// note: CDP front-end sends all profile property values values as strings (except booleans), but some profile properties
// like "age" only work with integers so they have to be changed to the right type to be used in unomi segments
// note for Number(): https://gomakethings.com/converting-strings-to-numbers-with-vanilla-javascript/
if (helpers_1.allSameType(propertyValue)) { // check if all values in array are of the same type
switch (expectedType) { // change type based on expected value check
case "string":
for (let index = 0; index < propertyValue.length; index++) { // go through array
propertyValue[index] = propertyValue[index].toString(); // convert all values to string
}
break;
case "number":
for (let index = 0; index < propertyValue.length; index++) { // go through array
propertyValue[index] = Number(propertyValue[index]); // convert all values to integer/float
}
break;
}
}
}
else { // value is not an array
switch (expectedType) { // change type based on expected value check
case "string":
propertyValue = propertyValue.toString(); // convert value to string
break;
case "number":
propertyValue = Number(propertyValue); // convert value to integer/float
break;
}
}
return propertyValue; // return property value in expected type
}
exports.setExpectedValue = setExpectedValue;
/**
* @function validatePropertyValue
* @param {PropertiesDefault} parameterValuesObject
*/
function validatePropertyValue(parameterValuesObject) {
let isValidPropertyValueUsage = false; // default validation status
let validatedParameterValuesObject = {
propertyName: parameterValuesObject.propertyName,
comparisonOperator: parameterValuesObject.comparisonOperator,
};
let validationError = "";
let indicesDate = [4, 7]; // indices for the '-' character in yyyy-mm-dd (works with ISO format too, eg: 2020-03-20T14:28:23.382748)
let indices; // array for storing indeces
let expectedType = checkExpectedValue(parameterValuesObject.propertyName); // get expected type for profile property in current subcondition
parameterValuesObject.propertyValue = setExpectedValue(expectedType, parameterValuesObject.propertyValue); // convert property value to expected type
switch (typeof (parameterValuesObject.propertyValue)) { // check type of given propertyValue
case "boolean":
Object.assign(validatedParameterValuesObject, { propertyValue: parameterValuesObject.propertyValue.toString() }); // add string property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
break;
case "number":
Object.assign(validatedParameterValuesObject, { propertyValueInteger: parameterValuesObject.propertyValue }); // add integer/float property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
break;
case "string":
indices = helpers_1.findAllIndicesOfCharacter(parameterValuesObject.propertyValue, "-"); // find all instances of "-" in property value
if (helpers_1.areArraysEqual(indicesDate, indices)) { // if indeces in arrays are equal, then property value is a date
Object.assign(validatedParameterValuesObject, { propertyValueDate: parameterValuesObject.propertyValue }); // add date property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
else {
if (parameterValuesObject.propertyValue.toString().startsWith("dateExpr::")) { // if value starts with 'dateExpr::', then property is a date expression
let isValidDateExpr = isValidDateExpression(parameterValuesObject.propertyValue.replace("dateExpr::", ""));
if (isValidDateExpr) {
Object.assign(validatedParameterValuesObject, { propertyValueDateExpr: parameterValuesObject.propertyValue.replace("dateExpr::", "") }); // add date expression property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
else {
isValidPropertyValueUsage = false; // set subcondition as invalid
validationError = "Date expression " + parameterValuesObject.propertyValue.replace("dateExpr::", "") + " is invalid."; // set error message
}
}
else {
Object.assign(validatedParameterValuesObject, { propertyValue: parameterValuesObject.propertyValue }); // add string property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
}
break;
case "object":
if (parameterValuesObject.propertyValue == undefined || parameterValuesObject.propertyValue == null) { // check if null or undefined, since null is treated as object by typeof
isValidPropertyValueUsage = true; // set subcondition as valid
}
if (parameterValuesObject.propertyValue instanceof Array) { // check if array, since array is treated as object by typeof
if (helpers_1.allSameType(parameterValuesObject.propertyValue)) { // check if all array values are of the same type
switch (typeof (parameterValuesObject.propertyValue[0])) { // check type of first element in array (rest of elements should be same value!)
case "number":
Object.assign(validatedParameterValuesObject, { propertyValuesInteger: parameterValuesObject.propertyValue }); // add integer/float property values to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
break;
case "string":
indices = helpers_1.findAllIndicesOfCharacter(parameterValuesObject.propertyValue[0], "-"); // find all instances of "-" in property value
if (helpers_1.areArraysEqual(indicesDate, indices)) { // if indeces in arrays are equal, then property value is a date
Object.assign(validatedParameterValuesObject, { propertyValuesDate: parameterValuesObject.propertyValue }); // add date property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
else {
if (parameterValuesObject.propertyValue[0].toString().startsWith("dateExpr::")) { // if value starts with 'dateExpr::', then property is a date expression
let values = [];
for (let index = 0; index < parameterValuesObject.propertyValue.length; index++) { // go through property values
values.push(parameterValuesObject.propertyValue[index].replace("dateExpr::", "")); // remove 'dateExpr::' prefix from property value and add to new list
}
Object.assign(validatedParameterValuesObject, { propertyValuesDateExpr: values }); // add date expression property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
else {
Object.assign(validatedParameterValuesObject, { propertyValues: parameterValuesObject.propertyValue }); // add string property value to validated object
isValidPropertyValueUsage = true; // set subcondition as valid
}
}
break;
}
}
}
break;
}
return {
isValidPropertyValueUsage: isValidPropertyValueUsage,
validatedParameterValuesObject: validatedParameterValuesObject,
validationError: validationError
};
}
exports.validatePropertyValue = validatePropertyValue;
/**
* @function validateComparisonOperatorUsage
* @param {PropertiesPossible} parameterValuesObject
*/
function validateComparisonOperatorUsage(parameterValuesObject) {
let isValidComparisonOperatorUsage = false; // default validation status
const comparisonOperators = helpers_1.getProfilePropertyComparisonOperators(); // get an array of comparison operators
const comparisonOperatorExists = comparisonOperators.includes(parameterValuesObject.comparisonOperator); // check if comparison operator in subcondition exists
if (comparisonOperatorExists) { // check if comparison operator exists
const parameterValuesObjectKeys = Object.keys(parameterValuesObject); // get an array with the names of the subcondition properties
const propertyNameExists = parameterValuesObjectKeys.includes("propertyName"); // check if property name exists
// check which property value keys exist
const propertyValueExists = parameterValuesObjectKeys.includes("propertyValue");
const propertyValueIntegerExists = parameterValuesObjectKeys.includes("propertyValueInteger");
const propertyValueDateExists = parameterValuesObjectKeys.includes("propertyValueDate");
const propertyValueDateExprExists = parameterValuesObjectKeys.includes("propertyValueDateExpr");
const propertyValuesExists = parameterValuesObjectKeys.includes("propertyValues");
const propertyValuesIntegerExists = parameterValuesObjectKeys.includes("propertyValuesInteger");
const propertyValuesDateExists = parameterValuesObjectKeys.includes("propertyValuesDate");
const propertyValuesDateExprExists = parameterValuesObjectKeys.includes("propertyValuesDateExpr");
switch (parameterValuesObject.comparisonOperator) { // check which comparison operator
// these comparisonOperators do not accept arrays as propertyValue
case "equals":
case "notEquals":
case "greaterThan":
case "greaterThanOrEqualTo":
case "lessThan":
case "lessThanOrEqualTo":
if (propertyValueExists || propertyValueIntegerExists || propertyValueDateExists || propertyValueDateExprExists) {
isValidComparisonOperatorUsage = true; // set comparison operator choice as valid
}
break;
// these comparisonOperators only accept a single string as propertyValue
case "contains":
case "notContains":
case "startsWith":
case "endsWith":
if (propertyValueExists) {
isValidComparisonOperatorUsage = true; // set comparison operator choice as valid
}
break;
// these comparisonOperators do not need a propertyValue
case "exists":
case "missing":
if (parameterValuesObjectKeys.length === 2 && comparisonOperatorExists && propertyNameExists) {
isValidComparisonOperatorUsage = true; // set comparison operator choice as valid
}
break;
// these comparisonOperators do not accept single values as propertyValue
case "between":
case "in":
case "notIn":
case "all":
case "hasSomeOf":
case "hasNoneOf":
if (propertyValuesExists || propertyValuesIntegerExists || propertyValuesDateExists || propertyValuesDateExprExists) {
isValidComparisonOperatorUsage = true; // set comparison operator choice as valid
}
break;
}
}
return isValidComparisonOperatorUsage; // return validation status of comparison operator
}
exports.validateComparisonOperatorUsage = validateComparisonOperatorUsage;
/**
* @function validateMatchType
* @param {PropertiesDefault} subConditionObject
*/
function validateMatchType(subConditionObject) {
let isValidMatchType = false; // default validation status;
const matchTypes = helpers_1.getProfileSegmentMatchTypes(); // get an array of match types
const matchTypeExists = matchTypes.includes(subConditionObject.matchType); // check if match type in subcondition exists
if (matchTypeExists) { // check if comparison operator exists
isValidMatchType = true;
}
return isValidMatchType; // return validation status of comparison operator
}
exports.validateMatchType = validateMatchType;
/**
* @function validateSegmentsProperty
* @param {PropertiesDefault} subConditionObject
*/
function validateSegmentsProperty(subConditionObject) {
let isValidSegments = true; // default validation status;
let errors = new Array; // array to store validation errors
const regex = /[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}/; // regex to validate the segment uuid against: https://stackoverflow.com/a/18516125
for (let index = 0; index < subConditionObject.segments.length; index++) { // validate every segment uuid
const segment = subConditionObject.segments[index];
if (!regex.test(segment)) { // if the segment uuid is not a valid UUID4
isValidSegments = false; // segemnt uuid is not valid
errors.push({
value: "Segment " + segment + " is not a valid segment uuid. Segment uuids have to comply with the UUID4 specifications."
});
}
}
return [isValidSegments, errors]; // return validation status & errors
}
exports.validateSegmentsProperty = validateSegmentsProperty;
/**
* @function validateSegmentObject
* @param {UsedProperties} params
* @param {boolean} segmentUpdate
*/
function validateSegmentObject(params, segmentUpdate) {
let subConditions = new Array; // array for storing subconditions
let id; // segment (uu)id
let errors = new Array; // array to store errors
let areSubConditionsValid = true; // default validation status
for (let index = 0; index < params.subConditions.length; index++) { // go through subconditions
const subCondition = params.subConditions[index]; // get current subcondition
// right now we only support 2 conditionTypes: profilePropertyCondition & profileSegmentCondition
if (subCondition.conditionType == "profilePropertyCondition") {
const validationPropertyNameResponse = validatePropertyName(Object.assign({}, subCondition)); // use deepcopy of subCondition for rest of validation
const validationPropertyValueResponse = validatePropertyValue(validationPropertyNameResponse); // validate property value of subcondition
if (validationPropertyValueResponse.isValidPropertyValueUsage) { // check if valid property value
const isValidComparisonOperatorUsage = validateComparisonOperatorUsage(validationPropertyValueResponse.validatedParameterValuesObject); // validate comparison operator usage in subcondition
if (!isValidComparisonOperatorUsage) { // used propertyValue(s) and comparisonOperator combination is not valid
areSubConditionsValid = false; // set validation status as invalid
if (validationPropertyValueResponse.validationError == "") { // if no specific error
errors.push({
value: "comparisonOperator and propertyValue do not work together for property " + subCondition.propertyName
});
}
else { // if there is a specific error
errors.push({
value: validationPropertyValueResponse.validationError
});
}
}
}
else { // used propertyValue(s) is not valid
areSubConditionsValid = false; // set validation status as invalid
if (validationPropertyValueResponse.validationError == "") { // if no specific error
errors.push({
value: "propertyValue is not allowed for property " + subCondition.propertyName
});
}
else { // if there is a specific error
errors.push({
value: validationPropertyValueResponse.validationError
});
}
}
subConditions.push({
type: subCondition.conditionType,
parameterValues: validationPropertyValueResponse.validatedParameterValuesObject
});
}
else if (subCondition.conditionType == "profileSegmentCondition") {
let validatedSubCondition = {};
const isValidMatchType = validateMatchType(Object.assign({}, subCondition)); // use deepcopy of subCondition
if (isValidMatchType) { // check if valid matchType
validatedSubCondition.matchType = subCondition.matchType;
}
else { // used matchType is not valid
areSubConditionsValid = false;
errors.push({
value: "The matchType is not valid: " + subCondition.matchType
});
}
const [isValidSegments, segmentvalidationerrors] = validateSegmentsProperty(Object.assign({}, subCondition)); // use deepcopy of subCondition
if (isValidSegments) { // check if valid matchType
validatedSubCondition.segments = subCondition.segments;
}
else { // used matchType is not valid
areSubConditionsValid = false;
errors.push({
value: "The segments are not valid: " + subCondition.segments
});
if (segmentvalidationerrors.length > 0) { // if there is at least 1 error
for (let index in segmentvalidationerrors) {
errors.push(segmentvalidationerrors[index]); // add validation errors to array
}
}
}
subConditions.push({
type: subCondition.conditionType,
parameterValues: validatedSubCondition
});
}
else { // if there's a conditionType we don't support yet
areSubConditionsValid = false;
errors.push({
value: "The selected condition type is not supported: " + subCondition.conditionType
});
}
}
;
// if not valid subconditions, then return the user's original subconditions so the mistake(s) can be seen and corrected
if (!areSubConditionsValid)
subConditions = params.subConditions;
if (segmentUpdate)
id = params.id; // check if segment update function
else
id = helpers_1.generateUuid(); // create segment function
const segmentObject = {
metadata: {
id: id,
name: params.name,
description: params.description,
scope: params.scope,
tags: params.tags,
systemTags: params.systemTags
},
condition: {
type: "booleanCondition",
parameterValues: {
operator: params.operator,
subConditions: subConditions
}
}
};
const validatedSegmentObject = {
segmentObject: segmentObject,
errors: errors,
areSubConditionsValid: areSubConditionsValid
};
return validatedSegmentObject; // return validated segment
}
exports.validateSegmentObject = validateSegmentObject;
/**
* @function validateRequiredProps
* @param {string[]} required
* @param {{[key: string]: any}} props
*/
function validateRequiredProps(required, props) {
let missing = [];
for (const prop of required) {
if (!Object.keys(props).includes(prop) || props[prop] === null || props[prop] === undefined) {
missing.push(prop);
}
}
;
return {
valid: !missing.length,
missing,
};
}
exports.validateRequiredProps = validateRequiredProps;
function formatSubConditions(subConditions) {
let formattedSubConditions = new Array; // array to store formatted subconditions
for (let index = 0; index < subConditions.length; index++) { // go through all subconditions
let subCondition = subConditions[index]; // get current subcondition
if (subCondition.type == "booleanCondition") { // check if current subcondition is a nested subcondition
let _formattedSubConditions = formatSubConditions(subCondition.parameterValues.subConditions); // summon this function again to loop over nested subconditions
formattedSubConditions.push({
"operator": subCondition.parameterValues.operator,
"subConditions": _formattedSubConditions
});
}
else { // current subcondition is a normal subcondition
// right now we only support 2 conditionTypes: profilePropertyCondition & profileSegmentCondition
if (subCondition.type === "profilePropertyCondition") {
subCondition = formatPropertyName(subCondition); // format subcondition
formattedSubConditions.push(formatProfilePropertySubCondition(subCondition)); // add formatted subcondition to array
}
else if (subCondition.type === "profileSegmentCondition") {
formattedSubConditions.push(formatProfileSegmentSubCondition(subCondition)); // add formatted subcondition to array
}
}
}
return formattedSubConditions; // return formatted subcondition
}
exports.formatSubConditions = formatSubConditions;
function formatProfilePropertySubCondition(subCondition) {
// note: CDP front-end expects all subcondition property values (except booleans) as type string,
// because it sends them as string too and the validation happens in the unomi-sdk
const parameterValuesObjectKeys = Object.keys(subCondition.parameterValues); // get an array with the names of the subcondition properties
// check which property value keys exist
const propertyValueExists = parameterValuesObjectKeys.includes("propertyValue");
const propertyValueIntegerExists = parameterValuesObjectKeys.includes("propertyValueInteger");
const propertyValueDateExists = parameterValuesObjectKeys.includes("propertyValueDate");
const propertyValueDateExprExists = parameterValuesObjectKeys.includes("propertyValueDateExpr");
const propertyValuesExists = parameterValuesObjectKeys.includes("propertyValues");
const propertyValuesIntegerExists = parameterValuesObjectKeys.includes("propertyValuesInteger");
const propertyValuesDateExists = parameterValuesObjectKeys.includes("propertyValuesDate");
const propertyValuesDateExprExists = parameterValuesObjectKeys.includes("propertyValuesDateExpr");
let formattedSubCondition = {
propertyName: subCondition.parameterValues.propertyName,
comparisonOperator: subCondition.parameterValues.comparisonOperator,
conditionType: "profilePropertyCondition"
};
if (propertyValueExists) { // check if string property value in subcondition
if (subCondition.parameterValues.propertyValue === "true" || subCondition.parameterValues.propertyValue === "false") { // check if originally boolean
subCondition.parameterValues.propertyValue = JSON.parse(subCondition.parameterValues.propertyValue.toLowerCase()); // convert to boolean
}
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValue }); // add formatted property value
}
else if (propertyValueIntegerExists) { // check if integer/float property value in subcondition
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValueInteger.toString() }); // convert to string and add formatted property value
}
else if (propertyValueDateExists) { // check if date property value in subcondition
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValueDate.toString() }); // convert to string and add formatted property value
}
else if (propertyValueDateExprExists) { // check if date expression property value in subcondition
Object.assign(formattedSubCondition, { propertyValue: "dateExpr::" + subCondition.parameterValues.propertyValueDateExpr.toString() }); // convert to string and add formatted property value
}
else if (propertyValuesExists) { // check if string property values in subcondition
for (let index = 0; index < subCondition.parameterValues.propertyValues.length; index++) { // go through each element in property values
if (subCondition.parameterValues.propertyValues[index] === "true" || subCondition.parameterValues.propertyValues[index] === "false") { // check if originally boolean
subCondition.parameterValues.propertyValues[index] = JSON.parse(subCondition.parameterValues.propertyValues[index].toLowerCase()); // convert to boolean
}
else { // not originally boolean
subCondition.parameterValues.propertyValues[index] = subCondition.parameterValues.propertyValues[index].toString(); // convert to string
}
}
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValues }); // add formatted property value
}
else if (propertyValuesIntegerExists) { // check if integer/float property values in subcondition
for (let index = 0; index < subCondition.parameterValues.propertyValuesInteger.length; index++) { // go through each element in property values
subCondition.parameterValues.propertyValuesInteger[index] = subCondition.parameterValues.propertyValuesInteger[index].toString(); // convert to string
}
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValuesInteger }); // add formatted property value
}
else if (propertyValuesDateExists) { // check if date property values in subcondition
for (let index = 0; index < subCondition.parameterValues.propertyValuesDate.length; index++) { // go through each element in property values
subCondition.parameterValues.propertyValuesDate[index] = subCondition.parameterValues.propertyValuesDate[index].toString(); // convert to string
}
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValuesDate }); // add formatted property value
}
else if (propertyValuesDateExprExists) { // check if date expression property values in subcondition
for (let index = 0; index < subCondition.parameterValues.propertyValuesDateExpr.length; index++) { // go through each element in property values
subCondition.parameterValues.propertyValuesDateExpr[index] = "dateExpr::" + subCondition.parameterValues.propertyValuesDateExpr[index].toString(); // convert to string
}
Object.assign(formattedSubCondition, { propertyValue: subCondition.parameterValues.propertyValuesDateExpr }); // add formatted property value | add dateExpr prefix so front-end knows it's a date expression and not a timestamp
}
else {
Object.assign(formattedSubCondition, { propertyValue: null }); // add formatted property value
}
return formattedSubCondition; // return formatted subcondition
}
exports.formatProfilePropertySubCondition = formatProfilePropertySubCondition;
function formatProfileSegmentSubCondition(subCondition) {
let formattedSubCondition = {
matchType: subCondition.parameterValues.matchType,
segments: subCondition.parameterValues.segments,
conditionType: "profileSegmentCondition"
};
return formattedSubCondition; // return formatted subcondition
}
exports.formatProfileSegmentSubCondition = formatProfileSegmentSubCondition;
function reformatSegment(sdkObject, segment) {
// Sometimes segments exist where the condition is different from what is by default expected
// example of a normal segment condition:
// "condition": {
// "type": "booleanCondition",
// "parameterValues": {
// "operator": "or",
// "subConditions": [
// {
// "parameterValues": {
// "matchType": "in",
// "segments": ["9a3066ac-9259-42c9-b289-89d68fe12827"]
// },
// "type": "profileSegmentCondition"
// },
// {
// "parameterValues": {
// "propertyName": "properties.capture_properties.te__domain",
// "comparisonOperator": "equals",
// "propertyValue": "example.com"
// },
// "type": "profilePropertyCondition"
// }
// ]
// }
// }
// example of the different segment condition:
// "condition": {
// "parameterValues": {
// "propertyName": "properties.capture_properties.te__domain",
// "comparisonOperator": "equals",
// "propertyValue": "example.com"
// },
// "type": "profilePropertyCondition"
// }
// This happens when the condition had a profileSegmentCondition on a segment (let's say segment-001), but then that segment (segment-001) got delete
// When that segment (segment-001) gets deleted, the profileSegmentCondition on that segment also gets deleted
// then if after that, the original segment only has 1 subCondition left, then you get the 2nd format (even though that's not how you're supposed to make segment in Unomi, so seems like a Unomi bug somewhere, maybe)
// so, just because it can happen, we need to be able to also show these segments in the frontend instead of getting an error during the formatting of the segment for the frontend
// so we first check if either propertyName or matchType exists in the condition object, to see if it's a normal condition, or the specific case described above
// if it's the 2nd case, then we build our own object for the condition, the way it is supposed to be, and then our object gets validated and sent to the frontend
if ("propertyName" in segment.condition.parameterValues) {
segment.condition = {
"type": "booleanCondition",
"parameterValues": {
"operator": "and",
"subConditions": [
{
"parameterValues": {
"propertyName": segment.condition.parameterValues.propertyName,
"propertyValue": segment.condition.parameterValues.propertyValue,
"comparisonOperator": segment.condition.parameterValues.comparisonOperator
},
"type": segment.condition.type
}
]
}
};
}
if ("matchType" in segment.condition.parameterValues) {
segment.condition = {
"type": "booleanCondition",
"parameterValues": {
"operator": "and",
"subConditions": [
{
"parameterValues": {
"matchType": segment.condition.parameterValues.matchType,
"segments": segment.condition.parameterValues.segments,
},
"type": segment.condition.type
}
]
}
};
}
let formattedSubConditions = formatSubConditions(segment.condition.parameterValues.subConditions); // format subconditions in segment
const segmentObject = {
id: segment.metadata.id,
name: segment.metadata.name,
description: segment.metadata.description,
scope: segment.metadata.scope,
tags: segment.metadata.tags,
systemTags: segment.metadata.systemTags,
operator: segment.condition.parameterValues.operator,
subConditions: formattedSubConditions
};
sdkObject.responseData = segmentObject; // set object as response data
return sdkObject; // return unomi-sdk response object
}
exports.reformatSegment = reformatSegment;
/**
* @function isValidDateExpression
* @param {string} dateExpr
*/
function isValidDateExpression(dateExpr) {
// examples of what date expressions this regex allows
// * now+1y
// * now-22M
// * now
// * now+1y/y
// * now+2d+1w
// * now/d
// * now+1y+2M+3w+4d+5H+6m+7s
// * nowww
// * now+1y
// * now/d
//
// examples of what the regex does NOT allow
// * now+7d/d+89w/M/H/s
// * now+7lekkerkoek
// * nowww
// * no
const regex = /^now(|\/[yMwdhHms]|([+-][0-9]+[yMwdhHms](\/[yMwdhHms])?)+)$/; // regex to check valid date expressions
const found = dateExpr.match(regex); // check if the dateExpr meets the regex conditions (null if it doesn't, a list with the groups if it does)
var isValid = false; // default value
if (found != null) {
isValid = true; // if the dateExpr meets the regex conditions, then it is a valid date expression
}
return isValid; // return whether the dateExpr is valid or not
}
exports.isValidDateExpression = isValidDateExpression;
/**
* @function validateSegmentCountObject
* @param {SegmentProfileCountProperties} params
*/
function validateSegmentCountObject(params) {
let segments = new Array; // array for storing validated segments
let validatedOperator; // to store the validated operator in the end
let errors = new Array; // array to store validation errors
let isValid = true; // valid by default
const operator = params.operator; // get the operator from the object to validate
const regex = /[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}/; // regex to validate the segment uuid against: https://stackoverflow.com/a/18516125
for (let index = 0; index < params.segments.length; index++) { // validate every segment uuid
const segment = params.segments[index];
if (regex.test(segment)) { // if the segment uuid is a valid UUID4
segments.push(segment);
}
else { // if the segment uuid is not a valid UUID4
isValid = false;
errors.push({
value: "Segment " + segment + " is not a valid segment uuid. Segment uuids have to comply with the UUID4 specifications."
});
}
}
if (operator.toUpperCase() === "OR" || operator.toUpperCase() === "AND") { // validate the operator, options we support are 'OR' and 'AND'
validatedOperator = operator.toUpperCase();
}
else {
isValid = false;
errors.push({
value: "Operator " + operator + " is not a valid operator. Valid operators are 'OR' & 'AND'."
});
}
const validatedSegmentCountObject = {
segments: segments,
operator: validatedOperator,
isValid: isValid,
errors: errors
};
return validatedSegmentCountObject; // return validated object
}
exports.validateSegmentCountObject = validateSegmentCountObject;