Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:add support for datetime conditions match #94

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/api/src/routes/sdkRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const sdkRoutesWrapper = (
async (req: Request, res: Response) => {
try {
const flagKey = req.params.flagKey;
const isV2 = req.query.v2?.toString().toLowerCase() === 'true';
const flagContext = req.body.context;
const correlationId = req.body.correlationId;

Expand All @@ -73,6 +74,7 @@ export const sdkRoutesWrapper = (
flag,
flagContext,
correlationId,
isV2
);
setSuccessResponse(res, ApiResponseCodes.Success, resp);
} catch (error) {
Expand Down
124 changes: 105 additions & 19 deletions packages/api/src/services/sdkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
FlagModel,
ApiResponseCodes,
dateHelper,
DayTimeOperator,
NumericOperator,
} from "@switchfeat/core";
import { ConditionModel, StringOperator } from "@switchfeat/core";
import { v4 as uuidv4 } from "uuid";
Expand All @@ -23,6 +25,7 @@ export const evaluateFlag = async (
flag: FlagModel,
context: Record<string, string>,
correlationId: string,
isV2?: boolean
): Promise<EvaluateResponse> => {
const response: EvaluateResponse = {
match: false,
Expand All @@ -45,29 +48,71 @@ export const evaluateFlag = async (
}

let foundMatchCondition = false;
flag.rules.map((x) => {
if (!foundMatchCondition) {
const conditions = x.segment.conditions;
const matchCondition = conditions?.filter(
(y) => y.context === firstContextKey,
)[0];
if (matchCondition) {
if (isV2) {
const subEvaluate = (conditions, shouldEvaluateAll = false) => {
if (conditions && conditions.length === 0) {
return { isMatch: true, key: null, reason: ApiResponseCodes.ConditionNotFound };
}
for (const cond of conditions) {
//condition.context is considering as key
const contextValue = (context[cond.context] ?? '') as string;
const hasMatch = getMatchByCondition(
matchCondition,
cond,
contextValue,
);
response.match = hasMatch;
response.meta.segment = x.segment.key;
response.meta.condition = matchCondition.key;
foundMatchCondition = true;
response.reason = ApiResponseCodes.FlagMatch;
if (cond.debug) {
console.info("___cond___", cond);
console.info("___hasMatch___", hasMatch);
console.info("___contextValue___", contextValue);
}
if (!shouldEvaluateAll && hasMatch) {
return { isMatch: true, key: cond.key, reason: ApiResponseCodes.FlagMatch };
}
if (shouldEvaluateAll && !hasMatch) {
return { isMatch: false, key: cond.key, reason: ApiResponseCodes.FlagMatch };
}
}
return { isMatch: true, key: conditions.map(x => x.key), reason: ApiResponseCodes.NoMatchingCondition };
}
});

if (!foundMatchCondition) {
response.reason = ApiResponseCodes.NoMatchingCondition;
const _evaluate = (rules): void => {
for (const x of rules) {
const result = subEvaluate(x?.segment?.conditions, x?.segment?.matching === "all");
response.match = result.isMatch;
response.meta.segment = x.segment.key;
response.meta.condition = result.key;
response.reason = result.reason;
if (response.match == false) {
break;
}
};
}
_evaluate(flag.rules);
return response;
} else {
flag.rules.map((x) => {
if (!foundMatchCondition) {
const conditions = x.segment.conditions;
const matchCondition = conditions?.filter(
(y) => y.context === firstContextKey,
)[0];
if (matchCondition) {
const hasMatch = getMatchByCondition(
matchCondition,
contextValue,
);
response.match = hasMatch;
response.meta.segment = x.segment.key;
response.meta.condition = matchCondition.key;
foundMatchCondition = true;
response.reason = ApiResponseCodes.FlagMatch;
}
}
});

if (!foundMatchCondition) {
response.reason = ApiResponseCodes.NoMatchingCondition;
return response;
}
}
} catch (ex) {
response.reason = ApiResponseCodes.GenericError;
Expand All @@ -80,14 +125,52 @@ export const evaluateFlag = async (
return response;
};

const getMatchByCondition = (
const handleDateTimeMatcher = (
condition: ConditionModel,
contextValue: string,
): boolean => {
switch (condition.operator as DayTimeOperator) {
case "equals": return dateHelper.isSame(contextValue, condition.value);
case "notEquals": return !dateHelper.isSame(contextValue, condition.value);
case "before": return dateHelper.isBefore(contextValue, condition.value);
case "beforeOrAt": return dateHelper.isBeforeOrAt(contextValue, condition.value);
case "after": return dateHelper.isAfter(contextValue, condition.value);
case "afterOrAt": return dateHelper.isAfterOrAt(contextValue, condition.value);
}
return false;
};

const handleNumberMatcher = (
condition: ConditionModel,
contextValue: string,
): boolean => {
const parseContextValue = Number(contextValue);
const evaluateValue = Number(condition.value);
if (isNaN(parseContextValue) || isNaN(evaluateValue)) {
return false;
}
switch (condition.operator as NumericOperator) {
case "equals": return parseContextValue === evaluateValue;
case "notEquals": return parseContextValue !== evaluateValue;
case "gt": return parseContextValue > evaluateValue;
case "lt": return parseContextValue < evaluateValue;
case "lte": return parseContextValue <= evaluateValue;
case "gte": return parseContextValue >= evaluateValue;
}
return false;
};

const getMatchByCondition = (
condition: ConditionModel,
contextValue: string | boolean,
): boolean => {
switch (condition.conditionType) {
case "string": {
return stringConditionMatcher(condition, contextValue);
return stringConditionMatcher(condition, contextValue as string);
}
case 'datetime': return handleDateTimeMatcher(condition, contextValue as string);
case 'number': return handleNumberMatcher(condition, contextValue as string);
case 'boolean': return condition?.operator?.toString().toLowerCase() === contextValue?.toString().toLowerCase();
}

return false;
Expand All @@ -101,6 +184,9 @@ const stringConditionMatcher = (
case "equals": {
return contextValue === condition.value;
}
case "notEquals": return contextValue !== condition.value;
case "startsWith": return contextValue?.startsWith(condition.value) ?? false;
case "endsWith": return contextValue?.endsWith(condition.value) ?? false;
}

return false;
Expand Down
63 changes: 63 additions & 0 deletions packages/core/src/helpers/dateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,66 @@ export const diffFromUtcInDays = (date: string | undefined) => {
export const diffInMs = (startDate: DateTime, endDate: DateTime): number => {
return endDate.toMillis() - startDate.toMillis();
};

const possibleFormats = [
'yyyy-MM-dd',
'yyyy/MM/dd',
'dd/MM/yyyy',
'dd-MM-yyyy',
'LLL d, yyyy',
];

export const parseDate = (dateString: string): DateTime | null => {
for (const format of possibleFormats) {
const parsedDate = DateTime.fromFormat(dateString, format);
if (parsedDate.isValid) {
return parsedDate;
}
}
return null;
}

export const isSame = (date1: string, date2: string): boolean => {
const parsedDate1 = parseDate(date1);
const parsedDate2 = parseDate(date2);
if (!parsedDate1 || !parsedDate2) {
return false;
}
return parsedDate1.equals(parsedDate2);
}

export const isBefore = (date1: string, date2: string): boolean => {
const parsedDate1 = parseDate(date1);
const parsedDate2 = parseDate(date2);
if (!parsedDate1 || !parsedDate2) {
return false;
}
return parsedDate1 < parsedDate2;
}

export const isBeforeOrAt = (date1: string, date2: string): boolean => {
const parsedDate1 = parseDate(date1);
const parsedDate2 = parseDate(date2);
if (!parsedDate1 || !parsedDate2) {
return false;
}
return parsedDate1 <= parsedDate2;
}

export const isAfter = (date1: string, date2: string): boolean => {
const parsedDate1 = parseDate(date1);
const parsedDate2 = parseDate(date2);
if (!parsedDate1 || !parsedDate2) {
return false;
}
return parsedDate1 > parsedDate2;
}

export const isAfterOrAt = (date1: string, date2: string): boolean => {
const parsedDate1 = parseDate(date1);
const parsedDate2 = parseDate(date2);
if (!parsedDate1 || !parsedDate2) {
return false;
}
return parsedDate1 >= parsedDate2;
}