Skip to content

Commit 7db5ae0

Browse files
authored
feat: add new context filter operators (#453)
- Bumped version of `@bucketco/flag-evaluation` to 0.2.0 and added new context filter operators: `DATE_AFTER` and `DATE_BEFORE`. - Updated `@bucketco/node-sdk` to 1.9.0 to reflect the new version of `@bucketco/flag-evaluation`. - Updated `@bucketco/openfeature-node-provider` to use the latest `@bucketco/node-sdk` version.
1 parent e8e364e commit 7db5ae0

File tree

6 files changed

+311
-25
lines changed

6 files changed

+311
-25
lines changed

packages/flag-evaluation/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/flag-evaluation",
3-
"version": "0.1.5",
3+
"version": "0.2.0",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

packages/flag-evaluation/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ type ContextFilterOperator =
8585
| "LT"
8686
| "AFTER"
8787
| "BEFORE"
88+
| "DATE_AFTER"
89+
| "DATE_BEFORE"
8890
| "SET"
8991
| "NOT_SET"
9092
| "IS_TRUE"
@@ -342,6 +344,20 @@ export function evaluate(
342344
? fieldValueDate > daysAgo.getTime()
343345
: fieldValueDate < daysAgo.getTime();
344346
}
347+
case "DATE_AFTER":
348+
case "DATE_BEFORE": {
349+
const fieldValueDate = new Date(fieldValue).getTime();
350+
const valueDate = new Date(value).getTime();
351+
if (isNaN(fieldValueDate) || isNaN(valueDate)) {
352+
console.error(
353+
`${operator} operator requires valid date values: ${fieldValue}, ${value}`,
354+
);
355+
return false;
356+
}
357+
return operator === "DATE_AFTER"
358+
? fieldValueDate >= valueDate
359+
: fieldValueDate <= valueDate;
360+
}
345361
case "SET":
346362
return fieldValue !== "";
347363
case "NOT_SET":

packages/flag-evaluation/test/index.test.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,235 @@ describe("evaluate feature targeting integration ", () => {
471471
expect(res.value ?? false).toEqual(expected);
472472
},
473473
);
474+
475+
describe("DATE_AFTER and DATE_BEFORE in feature rules", () => {
476+
it("should evaluate DATE_AFTER operator in feature rules", () => {
477+
const res = evaluateFeatureRules({
478+
featureKey: "time_based_feature",
479+
rules: [
480+
{
481+
value: "enabled",
482+
filter: {
483+
type: "context",
484+
field: "user.createdAt",
485+
operator: "DATE_AFTER",
486+
values: ["2024-01-01"],
487+
},
488+
},
489+
],
490+
context: {
491+
user: {
492+
createdAt: "2024-06-15",
493+
},
494+
},
495+
});
496+
497+
expect(res).toEqual({
498+
featureKey: "time_based_feature",
499+
value: "enabled",
500+
context: {
501+
"user.createdAt": "2024-06-15",
502+
},
503+
ruleEvaluationResults: [true],
504+
reason: "rule #0 matched",
505+
missingContextFields: [],
506+
});
507+
});
508+
509+
it("should evaluate DATE_BEFORE operator in feature rules", () => {
510+
const res = evaluateFeatureRules({
511+
featureKey: "legacy_feature",
512+
rules: [
513+
{
514+
value: "enabled",
515+
filter: {
516+
type: "context",
517+
field: "user.lastLogin",
518+
operator: "DATE_BEFORE",
519+
values: ["2024-12-31"],
520+
},
521+
},
522+
],
523+
context: {
524+
user: {
525+
lastLogin: "2024-01-15",
526+
},
527+
},
528+
});
529+
530+
expect(res).toEqual({
531+
featureKey: "legacy_feature",
532+
value: "enabled",
533+
context: {
534+
"user.lastLogin": "2024-01-15",
535+
},
536+
ruleEvaluationResults: [true],
537+
reason: "rule #0 matched",
538+
missingContextFields: [],
539+
});
540+
});
541+
542+
it("should handle complex rules with DATE_AFTER and DATE_BEFORE in groups", () => {
543+
const res = evaluateFeatureRules({
544+
featureKey: "time_window_feature",
545+
rules: [
546+
{
547+
value: "active",
548+
filter: {
549+
type: "group",
550+
operator: "and",
551+
filters: [
552+
{
553+
type: "context",
554+
field: "event.startDate",
555+
operator: "DATE_AFTER",
556+
values: ["2024-01-01"],
557+
},
558+
{
559+
type: "context",
560+
field: "event.endDate",
561+
operator: "DATE_BEFORE",
562+
values: ["2024-12-31"],
563+
},
564+
],
565+
},
566+
},
567+
],
568+
context: {
569+
event: {
570+
startDate: "2024-06-01",
571+
endDate: "2024-11-30",
572+
},
573+
},
574+
});
575+
576+
expect(res).toEqual({
577+
featureKey: "time_window_feature",
578+
value: "active",
579+
context: {
580+
"event.startDate": "2024-06-01",
581+
"event.endDate": "2024-11-30",
582+
},
583+
ruleEvaluationResults: [true],
584+
reason: "rule #0 matched",
585+
missingContextFields: [],
586+
});
587+
});
588+
589+
it("should fail when DATE_AFTER condition is not met", () => {
590+
const res = evaluateFeatureRules({
591+
featureKey: "future_feature",
592+
rules: [
593+
{
594+
value: "enabled",
595+
filter: {
596+
type: "context",
597+
field: "user.signupDate",
598+
operator: "DATE_AFTER",
599+
values: ["2024-12-01"],
600+
},
601+
},
602+
],
603+
context: {
604+
user: {
605+
signupDate: "2024-01-15", // Too early
606+
},
607+
},
608+
});
609+
610+
expect(res).toEqual({
611+
featureKey: "future_feature",
612+
value: undefined,
613+
context: {
614+
"user.signupDate": "2024-01-15",
615+
},
616+
ruleEvaluationResults: [false],
617+
reason: "no matched rules",
618+
missingContextFields: [],
619+
});
620+
});
621+
622+
it("should fail when DATE_BEFORE condition is not met", () => {
623+
const res = evaluateFeatureRules({
624+
featureKey: "past_feature",
625+
rules: [
626+
{
627+
value: "enabled",
628+
filter: {
629+
type: "context",
630+
field: "user.lastActivity",
631+
operator: "DATE_BEFORE",
632+
values: ["2024-01-01"],
633+
},
634+
},
635+
],
636+
context: {
637+
user: {
638+
lastActivity: "2024-06-15", // Too late
639+
},
640+
},
641+
});
642+
643+
expect(res).toEqual({
644+
featureKey: "past_feature",
645+
value: undefined,
646+
context: {
647+
"user.lastActivity": "2024-06-15",
648+
},
649+
ruleEvaluationResults: [false],
650+
reason: "no matched rules",
651+
missingContextFields: [],
652+
});
653+
});
654+
655+
it("should work with optimized evaluator", () => {
656+
const evaluator = newEvaluator([
657+
{
658+
value: "time_sensitive",
659+
filter: {
660+
type: "group",
661+
operator: "and",
662+
filters: [
663+
{
664+
type: "context",
665+
field: "user.subscriptionDate",
666+
operator: "DATE_AFTER",
667+
values: ["2024-01-01"],
668+
},
669+
{
670+
type: "context",
671+
field: "user.trialEndDate",
672+
operator: "DATE_BEFORE",
673+
values: ["2024-12-31"],
674+
},
675+
],
676+
},
677+
},
678+
]);
679+
680+
const res = evaluator(
681+
{
682+
user: {
683+
subscriptionDate: "2024-03-15",
684+
trialEndDate: "2024-09-30",
685+
},
686+
},
687+
"subscription_feature",
688+
);
689+
690+
expect(res).toEqual({
691+
featureKey: "subscription_feature",
692+
value: "time_sensitive",
693+
context: {
694+
"user.subscriptionDate": "2024-03-15",
695+
"user.trialEndDate": "2024-09-30",
696+
},
697+
ruleEvaluationResults: [true],
698+
reason: "rule #0 matched",
699+
missingContextFields: [],
700+
});
701+
});
702+
});
474703
});
475704

476705
describe("operator evaluation", () => {
@@ -533,6 +762,65 @@ describe("operator evaluation", () => {
533762
expect(res).toEqual(expected);
534763
});
535764
}
765+
766+
describe("DATE_AFTER and DATE_BEFORE operators", () => {
767+
const dateTests = [
768+
// DATE_AFTER tests
769+
["2024-01-15", "DATE_AFTER", "2024-01-10", true], // After
770+
["2024-01-10", "DATE_AFTER", "2024-01-10", true], // Same date (>=)
771+
["2024-01-05", "DATE_AFTER", "2024-01-10", false], // Before
772+
["2024-12-31", "DATE_AFTER", "2024-01-01", true], // Much later
773+
["2023-01-01", "DATE_AFTER", "2024-01-01", false], // Much earlier
774+
775+
// DATE_BEFORE tests
776+
["2024-01-05", "DATE_BEFORE", "2024-01-10", true], // Before
777+
["2024-01-10", "DATE_BEFORE", "2024-01-10", true], // Same date (<=)
778+
["2024-01-15", "DATE_BEFORE", "2024-01-10", false], // After
779+
["2023-01-01", "DATE_BEFORE", "2024-01-01", true], // Much earlier
780+
["2024-12-31", "DATE_BEFORE", "2024-01-01", false], // Much later
781+
782+
// Edge cases with different date formats
783+
["2024-01-10T10:30:00Z", "DATE_AFTER", "2024-01-10T10:00:00Z", true], // ISO format with time
784+
["2024-01-10T09:30:00Z", "DATE_BEFORE", "2024-01-10T10:00:00Z", true], // ISO format with time
785+
[
786+
"2024-01-10T10:30:00.123Z",
787+
"DATE_AFTER",
788+
"2024-01-10T10:00:00.000Z",
789+
true,
790+
], // ISO format with time and milliseconds
791+
[
792+
"2024-01-10T09:30:00.123Z",
793+
"DATE_BEFORE",
794+
"2024-01-10T10:00:00.000Z",
795+
true,
796+
], // ISO format with time and milliseconds
797+
["01/15/2024", "DATE_AFTER", "01/10/2024", true], // US format
798+
["01/05/2024", "DATE_BEFORE", "01/10/2024", true], // US format
799+
] as const;
800+
801+
for (const [fieldValue, operator, filterValue, expected] of dateTests) {
802+
it(`evaluates '${fieldValue}' ${operator} '${filterValue}' = ${expected}`, () => {
803+
const res = evaluate(fieldValue, operator, [filterValue]);
804+
expect(res).toEqual(expected);
805+
});
806+
}
807+
808+
it("handles invalid date formats gracefully", () => {
809+
// Invalid dates should result in NaN comparisons and return false
810+
expect(evaluate("invalid-date", "DATE_AFTER", ["2024-01-10"])).toBe(
811+
false,
812+
);
813+
expect(evaluate("2024-01-10", "DATE_AFTER", ["invalid-date"])).toBe(
814+
false,
815+
);
816+
expect(evaluate("invalid-date", "DATE_BEFORE", ["2024-01-10"])).toBe(
817+
false,
818+
);
819+
expect(evaluate("2024-01-10", "DATE_BEFORE", ["invalid-date"])).toBe(
820+
false,
821+
);
822+
});
823+
});
536824
});
537825

538826
describe("rollout hash", () => {

packages/node-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@
4444
"vitest": "~1.6.0"
4545
},
4646
"dependencies": {
47-
"@bucketco/flag-evaluation": "0.1.4"
47+
"@bucketco/flag-evaluation": "0.2.0"
4848
}
4949
}

packages/openfeature-node-provider/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"vitest": "~1.6.0"
5151
},
5252
"dependencies": {
53-
"@bucketco/node-sdk": "1.8.4"
53+
"@bucketco/node-sdk": "1.9.0"
5454
},
5555
"peerDependencies": {
5656
"@openfeature/server-sdk": ">=1.16.1"

0 commit comments

Comments
 (0)