From ce8c3a75cfbf7b227f77e7ba324b3079df017e1a Mon Sep 17 00:00:00 2001 From: Fredrik Johansen Date: Mon, 18 Aug 2025 20:06:26 +0200 Subject: [PATCH 1/4] feat(types): changes "condition" property to "conditions" (array) BREAKING CHANGE: The "condition" type has been changed to be an array This means that from now on, you'll need to provide a single or multiple conditions in an array. The conditions logic is based on an "OR" condition, so only one of the conditions needs to be true. If all of the conditions are false, then the flag is returning false. --- src/types/FeatureFlag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/FeatureFlag.ts b/src/types/FeatureFlag.ts index 17c4e7c..db90a4d 100644 --- a/src/types/FeatureFlag.ts +++ b/src/types/FeatureFlag.ts @@ -2,5 +2,5 @@ import { Condition } from "./Condition"; export type FeatureFlag = { name: FlagNames; - condition: Condition; + conditions: Condition[]; }; From 66993b0a4b0532364e4c90a7dc4a7d9d25826b33 Mon Sep 17 00:00:00 2001 From: Fredrik Johansen Date: Mon, 18 Aug 2025 20:09:35 +0200 Subject: [PATCH 2/4] feat(helpers): adds support for multiple conditions Adds support for multiple conditions in the "isEnabled" helper function. It's a return fast approach, when means only one of the conditions needs to be true. This also means that the conditions are thought of as an "OR" statement. --- src/helpers/isEnabled.ts | 73 ++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/helpers/isEnabled.ts b/src/helpers/isEnabled.ts index 38d8fbc..92f407a 100644 --- a/src/helpers/isEnabled.ts +++ b/src/helpers/isEnabled.ts @@ -25,37 +25,52 @@ export const isEnabled = ({ return false; } - const { attribute, expectedValue, type: conditionType } = flag.condition; + let isEnabled: boolean = false; + for (const condition of flag.conditions) { + if (isEnabled) return true; // Return fast - if (!(attribute in property)) { - return false; - } + const { attribute, expectedValue, type: conditionType } = condition; - const value = property[attribute]; - - switch (conditionType) { - case "equal": - return equalCondition({ value, expectedValue }); - case "contains": - return containsCondition({ value, expectedValue }); - case "startsWith": - return startsWithCondition({ value, expectedValue }); - case "endsWith": - return endsWithCondition({ value, expectedValue }); - case "percentage": - return percentageCondition({ - featureName, - value, - expectedValue, - }); - case "greaterThan": - return greaterThanCondition({ value, expectedValue }); - case "lessThan": - return lessThanCondition({ value, expectedValue }); - case "regex": - return regexCondition({ value, expectedValue }); - default: - conditionType satisfies never; + if (!(attribute in property)) { return false; + } + + const value = property[attribute]; + + switch (conditionType) { + case "equal": + isEnabled = equalCondition({ value, expectedValue }); + continue; + case "contains": + isEnabled = containsCondition({ value, expectedValue }); + continue; + case "startsWith": + isEnabled = startsWithCondition({ value, expectedValue }); + continue; + case "endsWith": + isEnabled = endsWithCondition({ value, expectedValue }); + continue; + case "percentage": + isEnabled = percentageCondition({ + featureName, + value, + expectedValue, + }); + continue; + case "greaterThan": + isEnabled = greaterThanCondition({ value, expectedValue }); + continue; + case "lessThan": + isEnabled = lessThanCondition({ value, expectedValue }); + continue; + case "regex": + isEnabled = regexCondition({ value, expectedValue }); + continue; + default: + conditionType satisfies never; + return false; + } } + + return isEnabled; }; From 12cc404193e183ddbf47f5edfe36e1dcb7945533 Mon Sep 17 00:00:00 2001 From: Fredrik Johansen Date: Mon, 18 Aug 2025 20:11:10 +0200 Subject: [PATCH 3/4] fix(tests): updates tests to align with new "conditions" structure - Wraps all the conditions into an array (a single condition) - Adds a new test with multiple conditions in the index.test.ts file --- src/conditions/contains/contains.test.ts | 12 ++-- src/conditions/endsWith/endsWith.test.ts | 12 ++-- src/conditions/equal/equal.test.ts | 12 ++-- .../greaterThan/greaterThan.test.ts | 12 ++-- src/conditions/lessThan/lessThan.test.ts | 12 ++-- src/conditions/percentage/percentage.test.ts | 12 ++-- src/conditions/regex/regex.test.ts | 12 ++-- src/conditions/startsWith/startsWith.test.ts | 12 ++-- src/index.test.ts | 63 ++++++++++++++----- 9 files changed, 104 insertions(+), 55 deletions(-) diff --git a/src/conditions/contains/contains.test.ts b/src/conditions/contains/contains.test.ts index d354f0b..18a4295 100644 --- a/src/conditions/contains/contains.test.ts +++ b/src/conditions/contains/contains.test.ts @@ -28,11 +28,13 @@ describe("Condition - Contains", () => { flags: [ { name: "admin-dashboard", - condition: { - type: "contains", - attribute: "roles", - expectedValue: "admin", - }, + conditions: [ + { + type: "contains", + attribute: "roles", + expectedValue: "admin", + }, + ], }, ], }); diff --git a/src/conditions/endsWith/endsWith.test.ts b/src/conditions/endsWith/endsWith.test.ts index db6d015..4905076 100644 --- a/src/conditions/endsWith/endsWith.test.ts +++ b/src/conditions/endsWith/endsWith.test.ts @@ -28,11 +28,13 @@ describe("Condition - Ends With", () => { flags: [ { name: "example-emails", - condition: { - type: "endsWith", - attribute: "email", - expectedValue: "example.com", - }, + conditions: [ + { + type: "endsWith", + attribute: "email", + expectedValue: "example.com", + }, + ], }, ], }); diff --git a/src/conditions/equal/equal.test.ts b/src/conditions/equal/equal.test.ts index 46930f4..1a95a7f 100644 --- a/src/conditions/equal/equal.test.ts +++ b/src/conditions/equal/equal.test.ts @@ -28,11 +28,13 @@ describe("Condition - Equal", () => { flags: [ { name: "is-admin", - condition: { - type: "equal", - attribute: "isAdmin", - expectedValue: true, - }, + conditions: [ + { + type: "equal", + attribute: "isAdmin", + expectedValue: true, + }, + ], }, ], }); diff --git a/src/conditions/greaterThan/greaterThan.test.ts b/src/conditions/greaterThan/greaterThan.test.ts index bcf696a..7a956d1 100644 --- a/src/conditions/greaterThan/greaterThan.test.ts +++ b/src/conditions/greaterThan/greaterThan.test.ts @@ -36,11 +36,13 @@ describe("Condition - Greater Than", () => { flags: [ { name: "is-adult", - condition: { - type: "greaterThan", - attribute: "age", - expectedValue: 21, - }, + conditions: [ + { + type: "greaterThan", + attribute: "age", + expectedValue: 21, + }, + ], }, ], }); diff --git a/src/conditions/lessThan/lessThan.test.ts b/src/conditions/lessThan/lessThan.test.ts index c099582..84e05ec 100644 --- a/src/conditions/lessThan/lessThan.test.ts +++ b/src/conditions/lessThan/lessThan.test.ts @@ -36,11 +36,13 @@ describe("Condition - Less Than", () => { flags: [ { name: "is-teenager", - condition: { - type: "lessThan", - attribute: "age", - expectedValue: 21, - }, + conditions: [ + { + type: "lessThan", + attribute: "age", + expectedValue: 21, + }, + ], }, ], }); diff --git a/src/conditions/percentage/percentage.test.ts b/src/conditions/percentage/percentage.test.ts index 1544f63..6935502 100644 --- a/src/conditions/percentage/percentage.test.ts +++ b/src/conditions/percentage/percentage.test.ts @@ -57,11 +57,13 @@ describe("Condition - Percentage", () => { flags: [ { name: "new-feature", - condition: { - type: "percentage", - attribute: "userId", - expectedValue: 50, - }, + conditions: [ + { + type: "percentage", + attribute: "userId", + expectedValue: 50, + }, + ], }, ], }); diff --git a/src/conditions/regex/regex.test.ts b/src/conditions/regex/regex.test.ts index fb41b9d..0e8606e 100644 --- a/src/conditions/regex/regex.test.ts +++ b/src/conditions/regex/regex.test.ts @@ -36,11 +36,13 @@ describe("Condition - Regex", () => { flags: [ { name: "example-page", - condition: { - type: "regex", - attribute: "email", - expectedValue: /.+\@example.com/, - }, + conditions: [ + { + type: "regex", + attribute: "email", + expectedValue: /.+\@example.com/, + }, + ], }, ], }); diff --git a/src/conditions/startsWith/startsWith.test.ts b/src/conditions/startsWith/startsWith.test.ts index 02c74ad..28540c2 100644 --- a/src/conditions/startsWith/startsWith.test.ts +++ b/src/conditions/startsWith/startsWith.test.ts @@ -28,11 +28,13 @@ describe("Condition - Starts With", () => { flags: [ { name: "is-john", - condition: { - type: "startsWith", - attribute: "username", - expectedValue: "John", - }, + conditions: [ + { + type: "startsWith", + attribute: "username", + expectedValue: "John", + }, + ], }, ], }); diff --git a/src/index.test.ts b/src/index.test.ts index e3d0b55..b5dfa30 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -9,11 +9,13 @@ describe("End-To-End Tests - Library Tests", () => { flags: [ { name: "secret-page", - condition: { - type: "equal", - attribute: "email", - expectedValue: "test@example.com", - }, + conditions: [ + { + type: "equal", + attribute: "email", + expectedValue: "test@example.com", + }, + ], }, ], }); @@ -28,11 +30,13 @@ describe("End-To-End Tests - Library Tests", () => { flags: [ { name: "discount-price", - condition: { - type: "equal", - attribute: "price", - expectedValue: 100, - }, + conditions: [ + { + type: "equal", + attribute: "price", + expectedValue: 100, + }, + ], }, ], }); @@ -47,11 +51,40 @@ describe("End-To-End Tests - Library Tests", () => { flags: [ { name: "admin-dashboard", - condition: { - type: "equal", - attribute: "isAdmin", - expectedValue: true, - }, + conditions: [ + { + type: "equal", + attribute: "isAdmin", + expectedValue: true, + }, + ], + }, + ], + }); + expect(client.isEnabled("admin-dashboard")).toBe(true); + }); + + it("should return true when feature flag contains multiple conditions", () => { + const client = createFeatureFlagClient({ + property: { + isAdmin: false, + roles: "moderator,admin", + }, + flags: [ + { + name: "admin-dashboard", + conditions: [ + { + type: "equal", + attribute: "isAdmin", + expectedValue: true, + }, + { + type: "contains", + attribute: "roles", + expectedValue: "admin", + }, + ], }, ], }); From 9bea9a42ae4b2142e019920ef9e00f3723f3712e Mon Sep 17 00:00:00 2001 From: Fredrik Johansen Date: Mon, 18 Aug 2025 20:12:07 +0200 Subject: [PATCH 4/4] docs(documentation): updates docs to align with new conditions structure updates documentation to align with new multiple (array) conditions --- README.md | 28 ++++++++++++++++------------ docs/conditions/contains.md | 24 ++++++++++++++---------- docs/conditions/endsWith.md | 24 ++++++++++++++---------- docs/conditions/equal.md | 24 ++++++++++++++---------- docs/conditions/greaterThan.md | 12 +++++++----- docs/conditions/lessThan.md | 12 +++++++----- docs/conditions/percentage.md | 12 +++++++----- docs/conditions/regex.md | 12 +++++++----- docs/conditions/startsWith.md | 12 +++++++----- docs/usages/simple.md | 12 +++++++----- docs/usages/structured.md | 12 +++++++----- docs/usages/typed.md | 6 +++--- 12 files changed, 110 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index b8b83a8..340154b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ const client = createFeatureFlagClient({ ### Add properties to client -Secondly we want add some attributes we can use in the condition in each of the feature flags we are creating later. You are allowed to use `string`, `number` or `boolean` types as a property +Secondly we want add some attributes we can use in the condition(s) in each of the feature flags we are creating later. You are allowed to use `string`, `number` or `boolean` types as a property ```ts import { createFeatureFlagClient } from "toggle-kit"; @@ -82,7 +82,7 @@ const client = createFeatureFlagClient({ ### Create feature flag -Last but not least, we want to create our first flag. Here we specify a name for the feature flag and select the type of condition we want to evaluate upon. Then we select the property we want to evaluate, and an expected value. +Last but not least, we want to create our first flag. Here we specify a name for the feature flag and select the type of condition(s) we want to evaluate upon. Then we select the property we want to evaluate, and an expected value. ```ts import { createFeatureFlagClient } from "toggle-kit"; @@ -97,11 +97,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", - condition: { - type: "equal", - attribute: "email", - expectedValue: "test@example.com", - }, + conditions: [ + { + type: "equal", + attribute: "email", + expectedValue: "test@example.com", + }, + ], }, ], }); @@ -124,11 +126,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", - condition: { - type: "equal", - attribute: "email", - expectedValue: "test@example.com", - }, + conditions: [ + { + type: "equal", + attribute: "email", + expectedValue: "test@example.com", + }, + ], }, ], }); diff --git a/docs/conditions/contains.md b/docs/conditions/contains.md index 3f9553f..0d9d788 100644 --- a/docs/conditions/contains.md +++ b/docs/conditions/contains.md @@ -13,19 +13,23 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-admin", - condition: { - type: "contains", - attribute: "roles", - expectedValue: "admin", - }, + conditions: [ + { + type: "contains", + attribute: "roles", + expectedValue: "admin", + }, + ], }, { name: "is-gmail", - condition: { - type: "contains", - attribute: "email", - expectedValue: "@gmail.com", - }, + conditions: [ + { + type: "contains", + attribute: "email", + expectedValue: "@gmail.com", + }, + ], }, ], }); diff --git a/docs/conditions/endsWith.md b/docs/conditions/endsWith.md index c2d1065..1635792 100644 --- a/docs/conditions/endsWith.md +++ b/docs/conditions/endsWith.md @@ -12,19 +12,23 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-example-mail", - condition: { - type: "endsWith", - attribute: "email", - expectedValue: "@example.com", - }, + conditions: [ + { + type: "endsWith", + attribute: "email", + expectedValue: "@example.com", + }, + ], }, { name: "is-google-mail", - condition: { - type: "endsWith", - attribute: "email", - expectedValue: "@gmail.com", - }, + conditions: [ + { + type: "endsWith", + attribute: "email", + expectedValue: "@gmail.com", + }, + ], }, ], }); diff --git a/docs/conditions/equal.md b/docs/conditions/equal.md index f90f7e9..e98750b 100644 --- a/docs/conditions/equal.md +++ b/docs/conditions/equal.md @@ -12,19 +12,23 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-john", - condition: { - type: "equal", - attribute: "username", - expectedValue: "JohnDoe", - }, + conditions: [ + { + type: "equal", + attribute: "username", + expectedValue: "JohnDoe", + }, + ], }, { name: "is-jane", - condition: { - type: "equal", - attribute: "username", - expectedValue: "JaneDoe", - }, + conditions: [ + { + type: "equal", + attribute: "username", + expectedValue: "JaneDoe", + }, + ], }, ], }); diff --git a/docs/conditions/greaterThan.md b/docs/conditions/greaterThan.md index eca1f3f..3eb28fd 100644 --- a/docs/conditions/greaterThan.md +++ b/docs/conditions/greaterThan.md @@ -12,11 +12,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-adult", - condition: { - type: "greaterThan", - attribute: "age", - expectedValue: 20, - }, + conditions: [ + { + type: "greaterThan", + attribute: "age", + expectedValue: 20, + }, + ], }, ], }); diff --git a/docs/conditions/lessThan.md b/docs/conditions/lessThan.md index 9e09c65..d7c7304 100644 --- a/docs/conditions/lessThan.md +++ b/docs/conditions/lessThan.md @@ -12,11 +12,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-child", - condition: { - type: "lessThan", - attribute: "age", - expectedValue: 21, - }, + conditions: [ + { + type: "lessThan", + attribute: "age", + expectedValue: 21, + }, + ], }, ], }); diff --git a/docs/conditions/percentage.md b/docs/conditions/percentage.md index 739719d..d083499 100644 --- a/docs/conditions/percentage.md +++ b/docs/conditions/percentage.md @@ -12,11 +12,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-lucky", - condition: { - type: "percentage", - attribute: "userId", - expectedValue: 50, - }, + conditions: [ + { + type: "percentage", + attribute: "userId", + expectedValue: 50, + }, + ], }, ], }); diff --git a/docs/conditions/regex.md b/docs/conditions/regex.md index 3a6944b..152f8c7 100644 --- a/docs/conditions/regex.md +++ b/docs/conditions/regex.md @@ -12,11 +12,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-gmail", - condition: { - type: "regex", - attribute: "email", - expectedValue: /.+@gmail\.com/, - }, + conditions: [ + { + type: "regex", + attribute: "email", + expectedValue: /.+@gmail\.com/, + }, + ], }, ], }); diff --git a/docs/conditions/startsWith.md b/docs/conditions/startsWith.md index 5a4f925..734fe05 100644 --- a/docs/conditions/startsWith.md +++ b/docs/conditions/startsWith.md @@ -12,11 +12,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "is-john", - condition: { - type: "startsWith", - attribute: "username", - expectedValue: "john", - }, + conditions: [ + { + type: "startsWith", + attribute: "username", + expectedValue: "john", + }, + ], }, ], }); diff --git a/docs/usages/simple.md b/docs/usages/simple.md index 7b48bf6..fb8bd3b 100644 --- a/docs/usages/simple.md +++ b/docs/usages/simple.md @@ -15,11 +15,13 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", // No autocompletion - condition: { - type: "equal", - attribute: "email", // No autocompletion - expectedValue: "test@example.com", - }, + conditions: [ + { + type: "equal", + attribute: "email", // No autocompletion + expectedValue: "test@example.com", + }, + ], }, ], }); diff --git a/docs/usages/structured.md b/docs/usages/structured.md index 40ce93b..6859382 100644 --- a/docs/usages/structured.md +++ b/docs/usages/structured.md @@ -39,11 +39,13 @@ type FlagNames = "secret-page"; const flags: FeatureFlag[] = [ { name: "secret-page", // <--- Autocompletion - condition: { - type: "equal", - attribute: "email", // <--- Autocompletion - expectedValue: "test@example.com", - }, + conditions: [ + { + type: "equal", + attribute: "email", // <--- Autocompletion + expectedValue: "test@example.com", + }, + ], }, ]; diff --git a/docs/usages/typed.md b/docs/usages/typed.md index 2edb1a9..3e6b110 100644 --- a/docs/usages/typed.md +++ b/docs/usages/typed.md @@ -28,7 +28,7 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", // <--- Autocompletion - condition: { + conditions: { type: "equal", attribute: "email", // <--- Autocompletion expectedValue: "test@example.com", @@ -62,7 +62,7 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", // <--- Autocompletion - condition: { + conditions: { type: "equal", attribute: "email", // <--- No Autocompletion expectedValue: "test@example.com", @@ -94,7 +94,7 @@ const client = createFeatureFlagClient({ flags: [ { name: "secret-page", // <--- No Autocompletion - condition: { + conditions: { type: "equal", attribute: "email", // <--- No Autocompletion expectedValue: "test@example.com",