From 4040befbc91fa851041f1a8dfbd433a27a9f64da Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 17:45:00 +0200 Subject: [PATCH 01/25] feat: wip-working-first-version --- .gitmodules | 2 +- CHANGELOG.md | 329 ++++---- .../evaluationContext.types.ts | 233 ++++++ flagsmith-engine/evaluationContext/mappers.ts | 192 +++++ flagsmith-engine/evaluationContext/models.ts | 42 + flagsmith-engine/evaluationContext/types.ts | 233 ++++++ .../evaluationResult.types.ts | 290 +++++++ flagsmith-engine/evaluationResult/models.ts | 43 + flagsmith-engine/evaluationResult/types.ts | 290 +++++++ flagsmith-engine/features/models.ts | 5 +- flagsmith-engine/features/util.ts | 26 + flagsmith-engine/index.ts | 144 ++-- flagsmith-engine/segments/evaluators.ts | 161 ++-- flagsmith-engine/segments/models.ts | 74 +- flagsmith-engine/utils/hashing/index.ts | 4 +- package-lock.json | 758 ++++++++++++++++-- package.json | 7 +- sdk/index.ts | 36 +- sdk/models.ts | 36 +- tests/engine/e2e/engine.test.ts | 32 +- tests/engine/engine-tests/engine-test-data | 2 +- tests/engine/unit/engine.test.ts | 167 ++-- .../unit/segments/segment_evaluators.test.ts | 43 +- tests/engine/unit/utils.ts | 2 +- tests/engine/unit/utils/utils.test.ts | 14 +- 25 files changed, 2685 insertions(+), 480 deletions(-) create mode 100644 flagsmith-engine/evaluationContext/evaluationContext.types.ts create mode 100644 flagsmith-engine/evaluationContext/mappers.ts create mode 100644 flagsmith-engine/evaluationContext/models.ts create mode 100644 flagsmith-engine/evaluationContext/types.ts create mode 100644 flagsmith-engine/evaluationResult/evaluationResult.types.ts create mode 100644 flagsmith-engine/evaluationResult/models.ts create mode 100644 flagsmith-engine/evaluationResult/types.ts diff --git a/.gitmodules b/.gitmodules index bba15b6..d7c6389 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/engine/engine-tests/engine-test-data"] path = tests/engine/engine-tests/engine-test-data url = git@github.com:Flagsmith/engine-test-data.git - branch = v1.0.0 + branch = feat/context-values-intensifies diff --git a/CHANGELOG.md b/CHANGELOG.md index 133c163..1e0ca02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,83 +1,86 @@ + # [v6.1.0](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v6.1.0) - 2025-06-18 ## What's Changed -* Bump undici from 6.21.1 to 6.21.2 by [@dependabot](https://github.com/dependabot) in [#184](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/184) -* feat: Export FeatureModel to enable custom offline handler by [@phiggins](https://github.com/phiggins) in [#187](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/187) -* Update test running instructions in README and other housekeeping by [@phiggins](https://github.com/phiggins) in [#186](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/186) -* Bump vite from 5.4.18 to 5.4.19 by [@dependabot](https://github.com/dependabot) in [#185](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/185) -* feat: Export BaseFlag, FlagsmithConfig, FlagsmithValue, TraitConfig types by [@rolodato](https://github.com/rolodato) in [#188](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/188) +- Bump undici from 6.21.1 to 6.21.2 by [@dependabot](https://github.com/dependabot) in [#184](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/184) +- feat: Export FeatureModel to enable custom offline handler by [@phiggins](https://github.com/phiggins) in [#187](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/187) +- Update test running instructions in README and other housekeeping by [@phiggins](https://github.com/phiggins) in [#186](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/186) +- Bump vite from 5.4.18 to 5.4.19 by [@dependabot](https://github.com/dependabot) in [#185](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/185) +- feat: Export BaseFlag, FlagsmithConfig, FlagsmithValue, TraitConfig types by [@rolodato](https://github.com/rolodato) in [#188](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/188) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v6.0.1...v6.1.0 [Changes][v6.1.0] - + # [v6.0.1](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v6.0.1) - 2025-04-24 ## What's Changed -* Remove uses of `any` in models.ts by [@phiggins](https://github.com/phiggins) in [#180](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/180) -* Bump esbuild from 0.14.54 to 0.25.0 by [@dependabot](https://github.com/dependabot) in [#175](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/175) -* Bump vite from 5.4.14 to 5.4.18 by [@dependabot](https://github.com/dependabot) in [#182](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/182) + +- Remove uses of `any` in models.ts by [@phiggins](https://github.com/phiggins) in [#180](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/180) +- Bump esbuild from 0.14.54 to 0.25.0 by [@dependabot](https://github.com/dependabot) in [#175](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/175) +- Bump vite from 5.4.14 to 5.4.18 by [@dependabot](https://github.com/dependabot) in [#182](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/182) ## New Contributors -* [@phiggins](https://github.com/phiggins) made their first contribution in [#180](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/180) + +- [@phiggins](https://github.com/phiggins) made their first contribution in [#180](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/180) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v6.0.0...v6.0.1 [Changes][v6.0.1] - + # [v6.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v6.0.0) - 2025-03-24 ## What's Changed ### BREAKING CHANGES -* `Flagsmith.environment` was removed. Use `getEnvironment` instead. This returns a Promise, and not a reference to the environment which could be uninitialised. -* `onEnvironmentChange` handlers can now be invoked with an `undefined` environment if an error occurred. -* The `Flagsmith` client now returns an error if initialised with local evaluation enabled but without a server-side SDK key. Previously, it would log an error and continue. + +- `Flagsmith.environment` was removed. Use `getEnvironment` instead. This returns a Promise, and not a reference to the environment which could be uninitialised. +- `onEnvironmentChange` handlers can now be invoked with an `undefined` environment if an error occurred. +- The `Flagsmith` client now returns an error if initialised with local evaluation enabled but without a server-side SDK key. Previously, it would log an error and continue. ### New features -* Added a new `requestRetryDelayMilliseconds` which controls how long the SDK will wait before retrying any failed HTTP requests. Previously, this was hard-coded to always be 1 second. -* Added a `getEnvironment` method which returns the SDK's current local environment state as a Promise. +- Added a new `requestRetryDelayMilliseconds` which controls how long the SDK will wait before retrying any failed HTTP requests. Previously, this was hard-coded to always be 1 second. +- Added a `getEnvironment` method which returns the SDK's current local environment state as a Promise. ### Bug fixes -* `getIdentityFlags` now uses any provided default flag handler if it fails, instead of just returning an error. -* Setting `environmentRefreshInterval` to `0` now prevents any environment polling from happening. -* Fixed a bug where if the SDK initially failed to fetch the environment document, then `getIdentityFlags` would always fail with an error even if the environment was later fetched successfully (https://github.com/Flagsmith/flagsmith-nodejs-client/issues/177). - - +- `getIdentityFlags` now uses any provided default flag handler if it fails, instead of just returning an error. +- Setting `environmentRefreshInterval` to `0` now prevents any environment polling from happening. +- Fixed a bug where if the SDK initially failed to fetch the environment document, then `getIdentityFlags` would always fail with an error even if the environment was later fetched successfully (https://github.com/Flagsmith/flagsmith-nodejs-client/issues/177). **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v5.1.1...v6.0.0 [Changes][v6.0.0] - + # [v5.1.1](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v5.1.1) - 2025-02-10 ## What's Changed -* Bump undici from 6.19.8 to 6.21.1 by [@dependabot](https://github.com/dependabot) in [#170](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/170) -* Bump vite from 5.4.8 to 5.4.14 by [@dependabot](https://github.com/dependabot) in [#171](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/171) -* Bump vitest and @vitest/coverage-v8 by [@dependabot](https://github.com/dependabot) in [#173](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/173) +- Bump undici from 6.19.8 to 6.21.1 by [@dependabot](https://github.com/dependabot) in [#170](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/170) +- Bump vite from 5.4.8 to 5.4.14 by [@dependabot](https://github.com/dependabot) in [#171](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/171) +- Bump vitest and @vitest/coverage-v8 by [@dependabot](https://github.com/dependabot) in [#173](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/173) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v5.1.0...v5.1.1 [Changes][v5.1.1] - + # [v5.1.0](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v5.1.0) - 2025-01-20 ## What's Changed -* feat: Allow configuring analytics API endpoint separate from flags API by [@rolodato](https://github.com/rolodato) in [#168](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/168) -* ci: Run tests on currently maintained Node LTS versions by [@rolodato](https://github.com/rolodato) in [#169](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/169) + +- feat: Allow configuring analytics API endpoint separate from flags API by [@rolodato](https://github.com/rolodato) in [#168](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/168) +- ci: Run tests on currently maintained Node LTS versions by [@rolodato](https://github.com/rolodato) in [#169](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/169) ## Deprecated @@ -87,34 +90,35 @@ The [`baseApiUrl` constructor argument of `AnalyticsProcessor`](https://www.tsdo [Changes][v5.1.0] - + # [v5.0.1](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v5.0.1) - 2025-01-14 ## What's Changed -* fix: Return 0 as number flag value instead of undefined by [@rolodato](https://github.com/rolodato) in [#167](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/167) +- fix: Return 0 as number flag value instead of undefined by [@rolodato](https://github.com/rolodato) in [#167](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/167) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v5.0.0...v5.0.1 [Changes][v5.0.1] - + # [v5.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v5.0.0) - 2024-11-28 ## What's Changed -* fix: Export offline handler types by [@rolodato](https://github.com/rolodato) in [#166](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/166) -* feat!: Simplify FlagsmithCache interface by [@rolodato](https://github.com/rolodato) in [#165](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/165) + +- fix: Export offline handler types by [@rolodato](https://github.com/rolodato) in [#166](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/166) +- feat!: Simplify FlagsmithCache interface by [@rolodato](https://github.com/rolodato) in [#165](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/165) ## BREAKING CHANGES The `FlagsmithCache` interface has been simplified. In practice, this will not affect most users: -* Removed `has` method -* Removed `ttl` parameter from `set` -* Changed `set` return type to `Promise` -* Changed `get` return type to `Promise` +- Removed `has` method +- Removed `ttl` parameter from `set` +- Changed `set` return type to `Promise` +- Changed `get` return type to `Promise` `FlagsmithCache` since 5.0.0: https://www.tsdocs.dev/docs/flagsmith-nodejs/5.0.0/interfaces/FlagsmithCache.html `FlagsmithCache` prior to 5.0.0: https://www.tsdocs.dev/docs/flagsmith-nodejs/4.0.0/interfaces/FlagsmithCache.html @@ -123,14 +127,15 @@ The `FlagsmithCache` interface has been simplified. In practice, this will not a [Changes][v5.0.0] - + # [v4.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v4.0.0) - 2024-11-07 ## What's Changed -* feat: Support transient identities and traits by [@novakzaballa](https://github.com/novakzaballa) in [#158](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/158) -* feat!: Custom fetch support, remove node-fetch, ESM+CJS dual build, migrate to vitest, TS fixes, test improvements by [@rolodato](https://github.com/rolodato) in [#162](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/162) -* feat!: Remove all uses of CJS, add named Flagsmith export by [@rolodato](https://github.com/rolodato) in [#163](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/163) + +- feat: Support transient identities and traits by [@novakzaballa](https://github.com/novakzaballa) in [#158](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/158) +- feat!: Custom fetch support, remove node-fetch, ESM+CJS dual build, migrate to vitest, TS fixes, test improvements by [@rolodato](https://github.com/rolodato) in [#162](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/162) +- feat!: Remove all uses of CJS, add named Flagsmith export by [@rolodato](https://github.com/rolodato) in [#163](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/163) ### BREAKING CHANGES @@ -142,255 +147,264 @@ In 3.x and earlier, `Flagsmith` is the default export: ```js // ES modules -import Flagsmith from 'flagsmith-nodejs' +import Flagsmith from 'flagsmith-nodejs'; ``` ```js // CommonJS -const Flagsmith = require('flagsmith-nodejs') +const Flagsmith = require('flagsmith-nodejs'); ``` In 4.x, you must use the named export: ```js // ES modules -import { Flagsmith } from 'flagsmith-nodejs' +import { Flagsmith } from 'flagsmith-nodejs'; ``` ```js // CommonJS -const { Flagsmith } = require('flagsmith-nodejs') +const { Flagsmith } = require('flagsmith-nodejs'); ``` **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.3.3...v4.0.0 [Changes][v4.0.0] - + # [v3.3.3](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.3.3) - 2024-07-12 ## What's Changed -* Cancel timeout when it is no longer needed by [@wheineman-sunrun](https://github.com/wheineman-sunrun) in [#141](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/141) + +- Cancel timeout when it is no longer needed by [@wheineman-sunrun](https://github.com/wheineman-sunrun) in [#141](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/141) ## New Contributors -* [@wheineman-sunrun](https://github.com/wheineman-sunrun) made their first contribution in [#141](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/141) + +- [@wheineman-sunrun](https://github.com/wheineman-sunrun) made their first contribution in [#141](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/141) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.3.2...v3.3.3 [Changes][v3.3.3] - + # [v3.3.2](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.3.2) - 2024-05-23 ## What's Changed -* fix: handle null traits for regex evaluations by [@matthewelwell](https://github.com/matthewelwell) in [#152](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/152) +- fix: handle null traits for regex evaluations by [@matthewelwell](https://github.com/matthewelwell) in [#152](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/152) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.3.1...v3.3.2 [Changes][v3.3.2] - + # [v3.3.1](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.3.1) - 2024-05-08 ## What's Changed -* fix: only flush analytics once if requested concurrently by [@rolodato](https://github.com/rolodato) in [#148](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/148) -* fix: error evaluating CONTAINS / NOT_CONTAINS for null traits by [@matthewelwell](https://github.com/matthewelwell) in [#150](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/150) -* Bump version 3.3.1 by [@matthewelwell](https://github.com/matthewelwell) in [#151](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/151) + +- fix: only flush analytics once if requested concurrently by [@rolodato](https://github.com/rolodato) in [#148](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/148) +- fix: error evaluating CONTAINS / NOT_CONTAINS for null traits by [@matthewelwell](https://github.com/matthewelwell) in [#150](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/150) +- Bump version 3.3.1 by [@matthewelwell](https://github.com/matthewelwell) in [#151](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/151) ## New Contributors -* [@rolodato](https://github.com/rolodato) made their first contribution in [#148](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/148) + +- [@rolodato](https://github.com/rolodato) made their first contribution in [#148](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/148) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.3.0...v3.3.1 [Changes][v3.3.1] - + # [Version 3.3.0 (v3.3.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.3.0) - 2024-04-19 ## What's Changed -* feat: Identity overrides in local evaluation mode by [@khvn26](https://github.com/khvn26) in [#143](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/143) -* Bump @babel/traverse from 7.17.3 to 7.23.2 by [@dependabot](https://github.com/dependabot) in [#137](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/137) -* chore: export FlagsmithConfig from index by [@novakzaballa](https://github.com/novakzaballa) in [#139](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/139) -* chore: remove examples by [@dabeeeenster](https://github.com/dabeeeenster) in [#145](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/145) +- feat: Identity overrides in local evaluation mode by [@khvn26](https://github.com/khvn26) in [#143](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/143) +- Bump @babel/traverse from 7.17.3 to 7.23.2 by [@dependabot](https://github.com/dependabot) in [#137](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/137) +- chore: export FlagsmithConfig from index by [@novakzaballa](https://github.com/novakzaballa) in [#139](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/139) +- chore: remove examples by [@dabeeeenster](https://github.com/dabeeeenster) in [#145](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/145) ## New Contributors -* [@khvn26](https://github.com/khvn26) made their first contribution in [#143](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/143) + +- [@khvn26](https://github.com/khvn26) made their first contribution in [#143](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/143) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.2.0...v3.3.0 [Changes][v3.3.0] - + # [Version 3.2.0 (v3.2.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.2.0) - 2023-10-25 ## What's Changed -* feat: offline-mode by [@novakzaballa](https://github.com/novakzaballa) in [#136](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/136) +- feat: offline-mode by [@novakzaballa](https://github.com/novakzaballa) in [#136](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/136) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.1.1...v3.2.0 [Changes][v3.2.0] - + # [Version 3.1.1 (v3.1.1)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.1.1) - 2023-08-21 ## What's Changed -* fix: Default requestTimeout by [@novakzaballa](https://github.com/novakzaballa) in [#133](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/133) +- fix: Default requestTimeout by [@novakzaballa](https://github.com/novakzaballa) in [#133](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/133) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.1.0...v3.1.1 [Changes][v3.1.1] - + # [Version 3.1.0 (v3.1.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.1.0) - 2023-08-07 ## What's Changed -* Add 10 secs by default to requestTimeoutSeconds by [@novakzaballa](https://github.com/novakzaballa) in [#128](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/128) -* Bump version to 3.1.0 by [@novakzaballa](https://github.com/novakzaballa) in [#129](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/129) -* Bump word-wrap from 1.2.3 to 1.2.4 by [@dependabot](https://github.com/dependabot) in [#127](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/127) -* Bump tough-cookie from 4.0.0 to 4.1.3 by [@dependabot](https://github.com/dependabot) in [#125](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/125) -* Lazily calculate the hash by [@eldar-gamisoniya](https://github.com/eldar-gamisoniya) in [#130](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/130) + +- Add 10 secs by default to requestTimeoutSeconds by [@novakzaballa](https://github.com/novakzaballa) in [#128](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/128) +- Bump version to 3.1.0 by [@novakzaballa](https://github.com/novakzaballa) in [#129](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/129) +- Bump word-wrap from 1.2.3 to 1.2.4 by [@dependabot](https://github.com/dependabot) in [#127](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/127) +- Bump tough-cookie from 4.0.0 to 4.1.3 by [@dependabot](https://github.com/dependabot) in [#125](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/125) +- Lazily calculate the hash by [@eldar-gamisoniya](https://github.com/eldar-gamisoniya) in [#130](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/130) ## New Contributors -* [@novakzaballa](https://github.com/novakzaballa) made their first contribution in [#128](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/128) -* [@eldar-gamisoniya](https://github.com/eldar-gamisoniya) made their first contribution in [#130](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/130) + +- [@novakzaballa](https://github.com/novakzaballa) made their first contribution in [#128](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/128) +- [@eldar-gamisoniya](https://github.com/eldar-gamisoniya) made their first contribution in [#130](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/130) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.0.1...v3.1.0 [Changes][v3.1.0] - + # [Version 3.0.1 (v3.0.1)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.0.1) - 2023-06-27 ## What's Changed -* Fix deploy action by [@kyle-ssg](https://github.com/kyle-ssg) in [#121](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/121) -* Bump semver from 7.3.7 to 7.5.2 by [@dependabot](https://github.com/dependabot) in [#122](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/122) +- Fix deploy action by [@kyle-ssg](https://github.com/kyle-ssg) in [#121](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/121) +- Bump semver from 7.3.7 to 7.5.2 by [@dependabot](https://github.com/dependabot) in [#122](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/122) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v3.0.0...v3.0.1 [Changes][v3.0.1] - + # [Version 3.0.0 (v3.0.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v3.0.0) - 2023-06-15 ## What's Changed -* **BREAKING CHANGE**: Ensure percentage split evaluations are consistent by [@matthewelwell](https://github.com/matthewelwell) in [#119](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/119) -WARNING: We modified the local evaluation behaviour. You may see different flags returned to identities attributed to your percentage split-based segments after upgrading to this version. +- **BREAKING CHANGE**: Ensure percentage split evaluations are consistent by [@matthewelwell](https://github.com/matthewelwell) in [#119](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/119) +WARNING: We modified the local evaluation behaviour. You may see different flags returned to identities attributed to your percentage split-based segments after upgrading to this version. **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.5.2...v3.0.0 [Changes][v3.0.0] - + # [Version 2.5.2 (v2.5.2)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.5.2) - 2023-03-07 ## What's Changed -* Fix timeout not using default flags by [@matthewelwell](https://github.com/matthewelwell) in [#112](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/112) -* Release 2.5.2 by [@matthewelwell](https://github.com/matthewelwell) in [#111](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/111) +- Fix timeout not using default flags by [@matthewelwell](https://github.com/matthewelwell) in [#112](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/112) +- Release 2.5.2 by [@matthewelwell](https://github.com/matthewelwell) in [#111](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/111) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.5.1...v2.5.2 [Changes][v2.5.2] - + # [Version 2.5.1 (v2.5.1)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.5.1) - 2023-01-06 ## What's Changed -* Ensure local evaluation returns consistent MV values by [@matthewelwell](https://github.com/matthewelwell) in [#103](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/103) -* Add logic to check for empty identifiers in `getIdentity___` methods by [@matthewelwell](https://github.com/matthewelwell) in [#104](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/104) -* Bump json5 from 2.2.0 to 2.2.3 by [@dependabot](https://github.com/dependabot) in [#101](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/101) -* Release 2.5.1 by [@matthewelwell](https://github.com/matthewelwell) in [#102](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/102) +- Ensure local evaluation returns consistent MV values by [@matthewelwell](https://github.com/matthewelwell) in [#103](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/103) +- Add logic to check for empty identifiers in `getIdentity___` methods by [@matthewelwell](https://github.com/matthewelwell) in [#104](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/104) +- Bump json5 from 2.2.0 to 2.2.3 by [@dependabot](https://github.com/dependabot) in [#101](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/101) +- Release 2.5.1 by [@matthewelwell](https://github.com/matthewelwell) in [#102](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/102) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.5.0...v2.5.1 [Changes][v2.5.1] - + # [Version 2.5.0 (v2.5.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.5.0) - 2023-01-05 ## What's Changed -* Bump json5 from 2.1.0 to 2.2.3 in /examples/caching by [@dependabot](https://github.com/dependabot) in [#100](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/100) -* Bump json5 from 2.1.0 to 2.2.3 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#99](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/99) -* Bump json5 from 2.1.0 to 2.2.3 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#98](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/98) -* Bump json5 from 2.1.0 to 2.2.3 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#97](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/97) -* Bump json5 from 2.1.0 to 2.2.3 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#96](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/96) -* Bump minimatch from 3.0.4 to 3.1.2 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#91](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/91) -* Bump decode-uri-component from 0.2.0 to 0.2.2 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#90](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/90) -* Bump decode-uri-component from 0.2.0 to 0.2.2 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#89](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/89) -* Bump minimatch from 3.0.4 to 3.1.2 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#88](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/88) -* Swallow errors arising from fetch in analytics by [@matthewelwell](https://github.com/matthewelwell) in [#95](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/95) -* Release/2.5.0 by [@matthewelwell](https://github.com/matthewelwell) in [#84](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/84) +- Bump json5 from 2.1.0 to 2.2.3 in /examples/caching by [@dependabot](https://github.com/dependabot) in [#100](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/100) +- Bump json5 from 2.1.0 to 2.2.3 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#99](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/99) +- Bump json5 from 2.1.0 to 2.2.3 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#98](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/98) +- Bump json5 from 2.1.0 to 2.2.3 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#97](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/97) +- Bump json5 from 2.1.0 to 2.2.3 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#96](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/96) +- Bump minimatch from 3.0.4 to 3.1.2 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#91](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/91) +- Bump decode-uri-component from 0.2.0 to 0.2.2 in /examples/basic by [@dependabot](https://github.com/dependabot) in [#90](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/90) +- Bump decode-uri-component from 0.2.0 to 0.2.2 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#89](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/89) +- Bump minimatch from 3.0.4 to 3.1.2 in /examples/api-proxy by [@dependabot](https://github.com/dependabot) in [#88](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/88) +- Swallow errors arising from fetch in analytics by [@matthewelwell](https://github.com/matthewelwell) in [#95](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/95) +- Release/2.5.0 by [@matthewelwell](https://github.com/matthewelwell) in [#84](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/84) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.4.1...v2.5.0 [Changes][v2.5.0] - + # [Version 2.4.1 (v2.4.1)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.4.1) - 2023-01-05 ## What's Changed -* Fix issue with local evaluation of multivariate flags by [@matthewelwell](https://github.com/matthewelwell) in [#87](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/87) -* Release 2.4.1 by [@matthewelwell](https://github.com/matthewelwell) in [#86](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/86) +- Fix issue with local evaluation of multivariate flags by [@matthewelwell](https://github.com/matthewelwell) in [#87](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/87) +- Release 2.4.1 by [@matthewelwell](https://github.com/matthewelwell) in [#86](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/86) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.4.0...v2.4.1 [Changes][v2.4.1] - + # [Version 2.4.0 (v2.4.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.4.0) - 2022-11-01 ## What's Changed -* Bump glob-parent and @babel/cli in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#67](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/67) -* Bump ajv from 6.10.2 to 6.12.6 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#69](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/69) -* Bump ansi-regex from 3.0.0 to 3.0.1 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#68](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/68) -* Bump glob-parent and @babel/cli in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#70](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/70) -* Bump ajv from 6.10.2 to 6.12.6 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#73](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/73) -* Bump browserslist from 4.6.6 to 4.21.3 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#72](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/72) -* Bump ansi-regex from 3.0.0 to 3.0.1 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#71](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/71) -* Feature/403/modulo segment operators by [@EdsnLoor](https://github.com/EdsnLoor) in [#76](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/76) -* Feature/1145/is set is not set segment operators by [@EdsnLoor](https://github.com/EdsnLoor) in [#75](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/75) -* Bump glob-parent and @babel/cli in /examples/caching by [@dependabot](https://github.com/dependabot) in [#74](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/74) -* Release 2.4.0 by [@matthewelwell](https://github.com/matthewelwell) in [#77](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/77) + +- Bump glob-parent and @babel/cli in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#67](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/67) +- Bump ajv from 6.10.2 to 6.12.6 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#69](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/69) +- Bump ansi-regex from 3.0.0 to 3.0.1 in /examples/local-evaluation by [@dependabot](https://github.com/dependabot) in [#68](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/68) +- Bump glob-parent and @babel/cli in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#70](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/70) +- Bump ajv from 6.10.2 to 6.12.6 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#73](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/73) +- Bump browserslist from 4.6.6 to 4.21.3 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#72](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/72) +- Bump ansi-regex from 3.0.0 to 3.0.1 in /examples/custom-fetch-agent by [@dependabot](https://github.com/dependabot) in [#71](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/71) +- Feature/403/modulo segment operators by [@EdsnLoor](https://github.com/EdsnLoor) in [#76](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/76) +- Feature/1145/is set is not set segment operators by [@EdsnLoor](https://github.com/EdsnLoor) in [#75](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/75) +- Bump glob-parent and @babel/cli in /examples/caching by [@dependabot](https://github.com/dependabot) in [#74](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/74) +- Release 2.4.0 by [@matthewelwell](https://github.com/matthewelwell) in [#77](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/77) ## New Contributors -* [@EdsnLoor](https://github.com/EdsnLoor) made their first contribution in [#76](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/76) + +- [@EdsnLoor](https://github.com/EdsnLoor) made their first contribution in [#76](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/76) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v2.3.0...v2.4.0 [Changes][v2.4.0] - + # [2.3.0 - Allow custom fetch agents, improve examples and types (v2.3.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.3.0) - 2022-08-31 Allows people to supply a custom agent when initialising Flagsmith, allowing for -- Network-related config such as keep-alive / socket timeouts -- Proxies such as https://www.npmjs.com/package/https-proxy-agent +- Network-related config such as keep-alive / socket timeouts +- Proxies such as https://www.npmjs.com/package/https-proxy-agent Exports Flagsmith constructor arguments as a type. @@ -398,12 +412,10 @@ Adds a few examples concentrating on common use cases. Closes [#29](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/29), [#20](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/20) - - [Changes][v2.3.0] - + # [2.1.0 ES import support](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/2.1.0) - 2022-07-22 Closes [#42](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/42) - you can now import Flagsmith as such @@ -414,71 +426,72 @@ import Flagsmith, {...types} from 'flagsmith-nodejs' [Changes][2.1.0] - + # [Version 2.0.4 (v2.0.4)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.0.4) - 2022-07-13 ## What's Changed -* Use featureName for analytics by [@matthewelwell](https://github.com/matthewelwell) in [#48](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/48) -* Bump minimist from 1.2.5 to 1.2.6 by [@dependabot](https://github.com/dependabot) in [#38](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/38) -* Bump node-fetch from 2.1.2 to 2.6.7 by [@dependabot](https://github.com/dependabot) in [#39](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/39) -* Bump handlebars from 4.7.3 to 4.7.7 in /example by [@dependabot](https://github.com/dependabot) in [#17](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/17) -* Release 2.0.4 by [@matthewelwell](https://github.com/matthewelwell) in [#47](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/47) +- Use featureName for analytics by [@matthewelwell](https://github.com/matthewelwell) in [#48](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/48) +- Bump minimist from 1.2.5 to 1.2.6 by [@dependabot](https://github.com/dependabot) in [#38](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/38) +- Bump node-fetch from 2.1.2 to 2.6.7 by [@dependabot](https://github.com/dependabot) in [#39](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/39) +- Bump handlebars from 4.7.3 to 4.7.7 in /example by [@dependabot](https://github.com/dependabot) in [#17](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/17) +- Release 2.0.4 by [@matthewelwell](https://github.com/matthewelwell) in [#47](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/47) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/2.0.3...v2.0.4 [Changes][v2.0.4] - + # [2.0.3](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/2.0.3) - 2022-07-11 Closes [#43](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/43) [Changes][2.0.3] - + # [Version 2.0.0 (v2.0.0)](https://github.com/Flagsmith/flagsmith-nodejs-client/releases/tag/v2.0.0) - 2022-06-07 ## What's Changed -* Removes console.log of response by [@muddylemon](https://github.com/muddylemon) in [#1](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/1) -* Make bullet-train flags stateless, fix binding. by [@kyle-ssg](https://github.com/kyle-ssg) in [#2](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/2) -* Adds getUserIdentity(), getTrait() and setTrait(). Promise rejection if identity not provided by [@lukefanning](https://github.com/lukefanning) in [#3](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/3) -* Update client to use new api endpoints by [@matthewelwell](https://github.com/matthewelwell) in [#4](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/4) -* Update index.js by [@obax](https://github.com/obax) in [#6](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/6) -* Update config.js by [@obax](https://github.com/obax) in [#5](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/5) -* Solving error in Function by [@palazari19](https://github.com/palazari19) in [#8](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/8) -* Bump handlebars from 4.0.12 to 4.7.3 in /example by [@dependabot](https://github.com/dependabot) in [#9](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/9) -* feat: renamed type file to vscode automatically detect bullet-train type by [@raryson](https://github.com/raryson) in [#11](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/11) -* Rebrand by [@kyle-ssg](https://github.com/kyle-ssg) in [#14](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/14) -* Preventing errors while using this SDK by [@eilgin](https://github.com/eilgin) in [#15](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/15) -* Add a cache options to reduce latency by [@eilgin](https://github.com/eilgin) in [#16](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/16) -* fallback to require('node-fetch').default by [@kyle-ssg](https://github.com/kyle-ssg) in [#21](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/21) -* Fix setTrait Return Type by [@beeme1mr](https://github.com/beeme1mr) in [#26](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/26) -* WIP: Node SDK v2 by [@dabeeeenster](https://github.com/dabeeeenster) in [#23](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/23) -* Update default URL to point to Edge API by [@matthewelwell](https://github.com/matthewelwell) in [#36](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/36) -* feat: add semver support for segment condition by [@yuriihorodnyi21](https://github.com/yuriihorodnyi21) in [#37](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/37) -* Release 2.0.0 by [@matthewelwell](https://github.com/matthewelwell) in [#35](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/35) + +- Removes console.log of response by [@muddylemon](https://github.com/muddylemon) in [#1](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/1) +- Make bullet-train flags stateless, fix binding. by [@kyle-ssg](https://github.com/kyle-ssg) in [#2](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/2) +- Adds getUserIdentity(), getTrait() and setTrait(). Promise rejection if identity not provided by [@lukefanning](https://github.com/lukefanning) in [#3](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/3) +- Update client to use new api endpoints by [@matthewelwell](https://github.com/matthewelwell) in [#4](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/4) +- Update index.js by [@obax](https://github.com/obax) in [#6](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/6) +- Update config.js by [@obax](https://github.com/obax) in [#5](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/5) +- Solving error in Function by [@palazari19](https://github.com/palazari19) in [#8](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/8) +- Bump handlebars from 4.0.12 to 4.7.3 in /example by [@dependabot](https://github.com/dependabot) in [#9](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/9) +- feat: renamed type file to vscode automatically detect bullet-train type by [@raryson](https://github.com/raryson) in [#11](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/11) +- Rebrand by [@kyle-ssg](https://github.com/kyle-ssg) in [#14](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/14) +- Preventing errors while using this SDK by [@eilgin](https://github.com/eilgin) in [#15](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/15) +- Add a cache options to reduce latency by [@eilgin](https://github.com/eilgin) in [#16](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/16) +- fallback to require('node-fetch').default by [@kyle-ssg](https://github.com/kyle-ssg) in [#21](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/21) +- Fix setTrait Return Type by [@beeme1mr](https://github.com/beeme1mr) in [#26](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/26) +- WIP: Node SDK v2 by [@dabeeeenster](https://github.com/dabeeeenster) in [#23](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/23) +- Update default URL to point to Edge API by [@matthewelwell](https://github.com/matthewelwell) in [#36](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/36) +- feat: add semver support for segment condition by [@yuriihorodnyi21](https://github.com/yuriihorodnyi21) in [#37](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/37) +- Release 2.0.0 by [@matthewelwell](https://github.com/matthewelwell) in [#35](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/35) ## New Contributors -* [@muddylemon](https://github.com/muddylemon) made their first contribution in [#1](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/1) -* [@lukefanning](https://github.com/lukefanning) made their first contribution in [#3](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/3) -* [@obax](https://github.com/obax) made their first contribution in [#6](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/6) -* [@palazari19](https://github.com/palazari19) made their first contribution in [#8](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/8) -* [@dependabot](https://github.com/dependabot) made their first contribution in [#9](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/9) -* [@raryson](https://github.com/raryson) made their first contribution in [#11](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/11) -* [@eilgin](https://github.com/eilgin) made their first contribution in [#15](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/15) -* [@beeme1mr](https://github.com/beeme1mr) made their first contribution in [#26](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/26) -* [@dabeeeenster](https://github.com/dabeeeenster) made their first contribution in [#23](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/23) -* [@yuriihorodnyi21](https://github.com/yuriihorodnyi21) made their first contribution in [#37](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/37) + +- [@muddylemon](https://github.com/muddylemon) made their first contribution in [#1](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/1) +- [@lukefanning](https://github.com/lukefanning) made their first contribution in [#3](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/3) +- [@obax](https://github.com/obax) made their first contribution in [#6](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/6) +- [@palazari19](https://github.com/palazari19) made their first contribution in [#8](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/8) +- [@dependabot](https://github.com/dependabot) made their first contribution in [#9](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/9) +- [@raryson](https://github.com/raryson) made their first contribution in [#11](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/11) +- [@eilgin](https://github.com/eilgin) made their first contribution in [#15](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/15) +- [@beeme1mr](https://github.com/beeme1mr) made their first contribution in [#26](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/26) +- [@dabeeeenster](https://github.com/dabeeeenster) made their first contribution in [#23](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/23) +- [@yuriihorodnyi21](https://github.com/yuriihorodnyi21) made their first contribution in [#37](https://github.com/Flagsmith/flagsmith-nodejs-client/pull/37) **Full Changelog**: https://github.com/Flagsmith/flagsmith-nodejs-client/commits/v2.0.0 [Changes][v2.0.0] - [v6.1.0]: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v6.0.1...v6.1.0 [v6.0.1]: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v6.0.0...v6.0.1 [v6.0.0]: https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v5.1.1...v6.0.0 diff --git a/flagsmith-engine/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluationContext/evaluationContext.types.ts new file mode 100644 index 0000000..aef9efa --- /dev/null +++ b/flagsmith-engine/evaluationContext/evaluationContext.types.ts @@ -0,0 +1,233 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +export type SegmentCondition = SegmentCondition1 | InSegmentCondition; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = SegmentCondition[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; + +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition1 { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts new file mode 100644 index 0000000..4ce5c7e --- /dev/null +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -0,0 +1,192 @@ +import { + Features, + Segments, + Traits, + EvaluationContext, + EnvironmentContext, + IdentityContext +} from './models.js'; +import { EnvironmentModel } from '../environments/models.js'; +import { IdentityModel } from '../identities/models.js'; +import { TraitModel } from '../identities/traits/models.js'; + +export function getEvaluationContext( + environment: EnvironmentModel, + identity?: IdentityModel, + overrideTraits?: TraitModel[] +): EvaluationContext { + const environmentContext = mapEnvironmentModelToEvaluationContext(environment); + const identityContext = identity + ? mapIdentityModelToIdentityContext(identity, overrideTraits) + : undefined; + + const context = { + ...environmentContext, + ...(identityContext && { identity: identityContext }), + segments: { + ...environmentContext.segments, + ...(identity && mapIdentityOverridesToSegments(identity)) + } + }; + + return context; +} + +function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): EvaluationContext { + const environmentContext: EnvironmentContext = { + key: environment.apiKey, + name: environment.project.name + }; + + const features: Features = {}; + for (const fs of environment.featureStates) { + const variants = + fs.multivariateFeatureStateValues.length > 0 + ? [...fs.multivariateFeatureStateValues] + .sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) + .map(mv => ({ + value: mv.multivariateFeatureOption.value, + weight: mv.percentageAllocation + })) + : undefined; + features[fs.feature.name] = { + key: fs.djangoID?.toString() || fs.featurestateUUID, + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + variants, + priority: fs.featureSegment?.priority + }; + } + + const segments: Segments = {}; + for (const segment of environment.project.segments) { + segments[segment.id.toString()] = { + key: segment.id.toString(), + name: segment.name, + rules: segment.rules.map(rule => mapSegmentRuleModelToRule(rule)), + overrides: + segment.featureStates.length > 0 + ? segment.featureStates.map(fs => ({ + key: fs.djangoID?.toString() || fs.featurestateUUID, + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + priority: fs.featureSegment?.priority + })) + : [] + }; + } + + return { + environment: environmentContext, + features, + segments + }; +} + +function mapIdentityModelToIdentityContext( + identity: IdentityModel, + overrideTraits?: TraitModel[] +): IdentityContext { + const traits = overrideTraits || identity.identityTraits; + const traitsContext: Traits = {}; + + for (const trait of traits) { + traitsContext[trait.traitKey] = trait.traitValue; + } + + return { + identifier: identity.identifier, + key: identity.djangoID?.toString() || identity.compositeKey, + traits: traitsContext + }; +} + +function mapSegmentRuleModelToRule(rule: any): any { + return { + type: rule.type, + conditions: rule.conditions.map((condition: any) => ({ + property: condition.property, + operator: condition.operator, + value: condition.value + })), + rules: rule.rules.map((subRule: any) => mapSegmentRuleModelToRule(subRule)) + }; +} + +export function createIdentityContext( + environmentKey: string, + identifier: string, + traits: { [key: string]: any } = {} +): IdentityContext { + return { + identifier, + key: `${environmentKey}_${identifier}`, + traits + }; +} + +export function addIdentityToEvaluationContext( + context: EvaluationContext, + identifier: string, + traits: { [key: string]: any } = {} +): EvaluationContext { + return { + ...context, + identity: createIdentityContext(context.environment.key, identifier, traits) + }; +} + +function mapRawSegmentRule(rule: any): any { + return { + type: rule.type, + conditions: rule.conditions?.map((condition: any) => ({ + property: condition.property, + operator: condition.operator, + value: condition.value + })), + rules: rule.rules?.map((subRule: any) => mapRawSegmentRule(subRule)) + }; +} + +function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { + const segments: Segments = {}; + + if (!identity.identityFeatures || identity.identityFeatures.length === 0) { + return segments; + } + + const overrides = identity.identityFeatures.map(fs => ({ + key: fs.djangoID?.toString() || fs.featurestateUUID, + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + priority: -Infinity + })); + + const segmentKey = `identity_override_${identity.identifier}`; + + segments[segmentKey] = { + key: segmentKey, + name: 'identity_overrides', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: identity.identifier + } + ] + } + ], + overrides + }; + + return segments; +} diff --git a/flagsmith-engine/evaluationContext/models.ts b/flagsmith-engine/evaluationContext/models.ts new file mode 100644 index 0000000..8b3cd9f --- /dev/null +++ b/flagsmith-engine/evaluationContext/models.ts @@ -0,0 +1,42 @@ +import type { + EnvironmentContext, + IdentityContext, + SegmentContext, + SegmentRule, + SegmentCondition, + InSegmentCondition, + FeatureContext, + FeatureValue as ContextFeatureValue, + Traits, + Features, + Segments +} from './evaluationContext.types.ts'; + +export type EnvironmentKey = EnvironmentContext['key']; +export type EnvironmentName = EnvironmentContext['name']; + +export type IdentityIdentifier = IdentityContext['identifier']; +export type IdentityKey = IdentityContext['key']; + +export type SegmentKey = SegmentContext['key']; +export type SegmentName = SegmentContext['name']; +export type SegmentRuleType = SegmentRule['type']; +export type ConditionOperator = SegmentCondition['operator'] | InSegmentCondition['operator']; +export type ConditionProperty = SegmentCondition['property']; +export type ConditionValue = SegmentCondition['value'] | InSegmentCondition['value']; + +export type FeatureKey = FeatureContext['feature_key']; +export type FeatureName = FeatureContext['name']; +export type FeatureEnabled = FeatureContext['enabled']; +export type FeatureValue = FeatureContext['value']; +export type FeaturePriority = FeatureContext['priority']; +export type FeatureVariants = FeatureContext['variants']; + +export type VariantValue = ContextFeatureValue['value']; +export type VariantWeight = ContextFeatureValue['weight']; + +export type TraitMap = Traits; +export type FeatureMap = Features; +export type SegmentMap = Segments; + +export type * from './evaluationContext.types.ts'; diff --git a/flagsmith-engine/evaluationContext/types.ts b/flagsmith-engine/evaluationContext/types.ts new file mode 100644 index 0000000..e671005 --- /dev/null +++ b/flagsmith-engine/evaluationContext/types.ts @@ -0,0 +1,233 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +export type SegmentCondition = SegmentCondition1 | InSegmentCondition; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = SegmentCondition[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string; +/** + * The value of the feature. + */ +export type Value3 = string; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; + +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition1 { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} diff --git a/flagsmith-engine/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluationResult/evaluationResult.types.ts new file mode 100644 index 0000000..fc0df3b --- /dev/null +++ b/flagsmith-engine/evaluationResult/evaluationResult.types.ts @@ -0,0 +1,290 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = (SegmentCondition | InSegmentCondition)[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; +/** + * Unique feature identifier. + */ +export type FeatureKey1 = string; +/** + * Feature name. + */ +export type Name3 = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled1 = boolean; +/** + * Feature flag value. + */ +export type Value4 = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * List of feature flags evaluated for the context. + */ +export type Flags = FlagResult[]; +/** + * Unique segment identifier. + */ +export type Key4 = string; +/** + * Segment name. + */ +export type Name4 = string; +/** + * List of segments which the provided context belongs to. + */ +export type Segments1 = SegmentResult[]; + +/** + * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. + */ +export interface EvaluationResult { + context: EvaluationContext; + flags: Flags; + segments: Segments1; + [k: string]: unknown; +} +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} +export interface FlagResult { + feature_key: FeatureKey1; + name: Name3; + enabled: Enabled1; + value?: Value4; + reason?: Reason; + [k: string]: unknown; +} +export interface SegmentResult { + key: Key4; + name: Name4; + [k: string]: unknown; +} diff --git a/flagsmith-engine/evaluationResult/models.ts b/flagsmith-engine/evaluationResult/models.ts new file mode 100644 index 0000000..0442a1e --- /dev/null +++ b/flagsmith-engine/evaluationResult/models.ts @@ -0,0 +1,43 @@ +import type { EvaluationContext } from '../evaluationContext/models.ts'; + +import type { + EvaluationResult as EvaluationContextResult, + FlagResult as EvaluationContextResultFlagResult, + SegmentResult, + SegmentCondition, + IdentityContext, + SegmentContext, + EnvironmentContext +} from './evaluationResult.types.ts'; + +export type EnvironmentKey = EnvironmentContext['key']; +export type EnvironmentName = EnvironmentContext['name']; + +export type IdentityIdentifier = IdentityContext['identifier']; +export type IdentityKey = IdentityContext['key']; + +export type SegmentKey = SegmentResult['key']; +export type SegmentName = SegmentResult['name']; +export type SegmentConditionOperator = SegmentCondition['operator']; +export type SegmentRuleType = SegmentContext['rules'][0]['type']; + +export type FeatureKey = EvaluationContextResultFlagResult['feature_key']; +export type FeatureName = EvaluationContextResultFlagResult['name']; +export type FeatureEnabled = EvaluationContextResultFlagResult['enabled']; +export type FeatureValue = EvaluationContextResultFlagResult['value']; +export type EvaluationReason = EvaluationContextResultFlagResult['reason']; + +export type EvaluationResultSegments = EvaluationContextResult['segments']; +export type EvaluationResultFlags = { + feature_key: FeatureKey; + name: FeatureName; + enabled: FeatureEnabled; + value: FeatureValue; + reason: EvaluationReason; +}[]; + +export type EvaluationResult = { + context: EvaluationContext; + flags: EvaluationResultFlags; + segments: EvaluationResultSegments; +}; diff --git a/flagsmith-engine/evaluationResult/types.ts b/flagsmith-engine/evaluationResult/types.ts new file mode 100644 index 0000000..fc0df3b --- /dev/null +++ b/flagsmith-engine/evaluationResult/types.ts @@ -0,0 +1,290 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = (SegmentCondition | InSegmentCondition)[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; +/** + * Unique feature identifier. + */ +export type FeatureKey1 = string; +/** + * Feature name. + */ +export type Name3 = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled1 = boolean; +/** + * Feature flag value. + */ +export type Value4 = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * List of feature flags evaluated for the context. + */ +export type Flags = FlagResult[]; +/** + * Unique segment identifier. + */ +export type Key4 = string; +/** + * Segment name. + */ +export type Name4 = string; +/** + * List of segments which the provided context belongs to. + */ +export type Segments1 = SegmentResult[]; + +/** + * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. + */ +export interface EvaluationResult { + context: EvaluationContext; + flags: Flags; + segments: Segments1; + [k: string]: unknown; +} +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} +export interface FlagResult { + feature_key: FeatureKey1; + name: Name3; + enabled: Enabled1; + value?: Value4; + reason?: Reason; + [k: string]: unknown; +} +export interface SegmentResult { + key: Key4; + name: Name4; + [k: string]: unknown; +} diff --git a/flagsmith-engine/features/models.ts b/flagsmith-engine/features/models.ts index 686fbed..1549e5d 100644 --- a/flagsmith-engine/features/models.ts +++ b/flagsmith-engine/features/models.ts @@ -1,5 +1,5 @@ import { randomUUID as uuidv4 } from 'node:crypto'; -import { getHashedPercentateForObjIds } from '../utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; export class FeatureModel { id: number; @@ -103,6 +103,7 @@ export class FeatureStateModel { const sortedF = this.multivariateFeatureStateValues.sort((a, b) => { return a.id - b.id; }); + for (const myValue of sortedF) { switch (myValue.percentageAllocation) { case 0: @@ -111,7 +112,7 @@ export class FeatureStateModel { return myValue.multivariateFeatureOption.value; default: if (percentageValue === undefined) { - percentageValue = getHashedPercentateForObjIds([ + percentageValue = getHashedPercentageForObjIds([ this.djangoID || this.featurestateUUID, identityID ]); diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index 0a19589..202f614 100644 --- a/flagsmith-engine/features/util.ts +++ b/flagsmith-engine/features/util.ts @@ -6,6 +6,9 @@ import { MultivariateFeatureStateValueModel } from './models.js'; +import { FeatureContext } from '../evaluationContext/models.js'; +import { getHashedPercentageForObjIds as getHashedPercentageForObjIds } from '../utils/hashing/index.js'; + export function buildFeatureModel(featuresModelJSON: any): FeatureModel { return new FeatureModel(featuresModelJSON.id, featuresModelJSON.name, featuresModelJSON.type); } @@ -46,3 +49,26 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment { return new FeatureSegment(featureSegmentJSON.priority); } + +export function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any { + if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { + return evaluateMultivariateFeature(feature, identityKey); + } + + return feature.value; +} + +function evaluateMultivariateFeature(feature: FeatureContext, identityKey?: string): any { + const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); + + let startPercentage = 0; + for (const variant of feature?.variants || []) { + const limit = startPercentage + variant.weight; + + if (startPercentage <= percentageValue && percentageValue < limit) { + return variant.value; + } + startPercentage = limit; + } + return feature.value; +} diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index fb641ee..0db0319 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,102 +1,80 @@ -import { EnvironmentModel } from './environments/models.js'; -import { FeatureStateModel } from './features/models.js'; -import { IdentityModel } from './identities/models.js'; -import { TraitModel } from './identities/traits/models.js'; +import { EvaluationContext, FeatureContext } from './evaluationContext/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { SegmentModel } from './segments/models.js'; -import { FeatureStateNotFound } from './utils/errors.js'; - +import { EvaluationResult, EvaluationResultFlags } from './evaluationResult/models.js'; +import { evaluateFeatureValue } from './features/util.js'; export { EnvironmentModel } from './environments/models.js'; -export { FeatureModel, FeatureStateModel } from './features/models.js'; export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; export { SegmentModel } from './segments/models.js'; -export { OrganisationModel } from './organisations/models.js'; - -function getIdentityFeatureStatesDict( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -) { - // Get feature states from the environment - const featureStates: { [key: number]: FeatureStateModel } = {}; - for (const fs of environment.featureStates) { - featureStates[fs.feature.id] = fs; - } +// 1. Mappers => Env/identities/segments => EvaluationContext +// 2. One entrypoint => getEvaluationResult +// 3. All these must be disappear - // Override with any feature states defined by matching segments - const identitySegments: SegmentModel[] = getIdentitySegments( - environment, - identity, - overrideTraits - ); - for (const matchingSegment of identitySegments) { - for (const featureState of matchingSegment.featureStates) { - if (featureStates[featureState.feature.id]) { - if (featureStates[featureState.feature.id].isHigherSegmentPriority(featureState)) { - continue; - } - } - featureStates[featureState.feature.id] = featureState; - } - } +type segmentOverride = { + feature: FeatureContext; + segmentName: string; +}; - // Override with any feature states defined directly the identity - for (const fs of identity.identityFeatures) { - if (featureStates[fs.feature.id]) { - featureStates[fs.feature.id] = fs; - } - } - return featureStates; -} +export function getEvaluationResult(context: EvaluationContext): EvaluationResult { + const segments: EvaluationResult['segments'] = []; + const segmentOverrides: Record = {}; + const DEFAULT_PRIORITY = Infinity; -export function getIdentityFeatureState( - environment: EnvironmentModel, - identity: IdentityModel, - featureName: string, - overrideTraits?: TraitModel[] -): FeatureStateModel { - const featureStates = getIdentityFeatureStatesDict(environment, identity, overrideTraits); + if (context.identity && context.segments) { + const identitySegments = getIdentitySegments(context); - const matchingFeature = Object.values(featureStates).filter( - f => f.feature.name === featureName - ); + for (const segment of identitySegments) { + segments.push({ key: segment.key, name: segment.name }); - if (matchingFeature.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); + if (segment.overrides) { + const overridesList = Array.isArray(segment.overrides) ? segment.overrides : []; + for (const override of overridesList) { + const currentOverride = segmentOverrides[override.feature_key]; + if ( + !currentOverride || + (override.priority ?? DEFAULT_PRIORITY) < + (currentOverride.feature.priority ?? DEFAULT_PRIORITY) + ) { + segmentOverrides[override.feature_key] = { + feature: override, + segmentName: segment.name + }; + } + } + } + } } - return matchingFeature[0]; -} - -export function getIdentityFeatureStates( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -): FeatureStateModel[] { - const featureStates = Object.values( - getIdentityFeatureStatesDict(environment, identity, overrideTraits) - ); + const flags: EvaluationResultFlags = []; + for (const feature of Object.values(context.features || {})) { + const segmentOverride = segmentOverrides[feature.feature_key]; + const finalFeature = segmentOverride ? segmentOverride.feature : feature; + const reason = getTargetingMatchReason(segmentOverride, segmentOverride?.segmentName); + const hasOverride = !!segmentOverride; - if (environment.project.hideDisabledFlags) { - return featureStates.filter(fs => !!fs.enabled); + flags.push({ + feature_key: finalFeature.feature_key, + name: finalFeature.name, + enabled: finalFeature.enabled, + value: hasOverride + ? finalFeature.value + : evaluateFeatureValue(finalFeature, context.identity?.key), + reason + }); } - return featureStates; -} - -export function getEnvironmentFeatureState(environment: EnvironmentModel, featureName: string) { - const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName); - if (featuresStates.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); - } + // Not sure if we need this - Keeping till confirmed hidedisabledflags is remote evaluation only + // const filteredFlags = hideDisabledFlags ? flags.filter(flag => flag.enabled) : flags; - return featuresStates[0]; + return { context, flags, segments }; } -export function getEnvironmentFeatureStates(environment: EnvironmentModel): FeatureStateModel[] { - if (environment.project.hideDisabledFlags) { - return environment.featureStates.filter(fs => !!fs.enabled); +const getTargetingMatchReason = (segmentOverride: segmentOverride, segmentName: string) => { + if (segmentOverride) { + // TURN INTO CONSTANT + return segmentOverride.segmentName === 'identity_overrides' + ? 'IDENTITY_OVERRIDE' + : `TARGETING_MATCH; segment=${segmentOverride.segmentName}`; } - return environment.featureStates; -} + return `DEFAULT`; +}; diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index f5d0081..e2638ce 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,76 +1,123 @@ -import { EnvironmentModel } from '../environments/models.js'; -import { IdentityModel } from '../identities/models.js'; -import { TraitModel } from '../identities/traits/models.js'; -import { getHashedPercentateForObjIds } from '../utils/hashing/index.js'; -import { PERCENTAGE_SPLIT, IS_SET, IS_NOT_SET } from './constants.js'; -import { SegmentConditionModel, SegmentModel, SegmentRuleModel } from './models.js'; - -export function getIdentitySegments( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -): SegmentModel[] { - return environment.project.segments.filter(segment => - evaluateIdentityInSegment(identity, segment, overrideTraits) +import { EvaluationContext, IdentityContext, SegmentContext } from '../evaluationContext/models.js'; +import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; +import { SegmentConditionModel } from './models.js'; +import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; +import { EvaluationResult } from '../evaluationResult/models.js'; + +export function getIdentitySegments(context: EvaluationContext): EvaluationResult['segments'] { + if (!context.identity || !context.segments) { + return []; + } + + return Object.values(context.segments).filter(segment => + evaluateIdentityInSegment(segment, context) ); } export function evaluateIdentityInSegment( - identity: IdentityModel, - segment: SegmentModel, - overrideTraits?: TraitModel[] + segment: SegmentContext, + context?: EvaluationContext ): boolean { - return ( + const result = segment.rules.length > 0 && - segment.rules.filter(rule => - traitsMatchSegmentRule( - overrideTraits || identity.identityTraits, - rule, - segment.id, - identity.djangoID || identity.compositeKey - ) - ).length === segment.rules.length - ); + segment.rules.filter(rule => { + const ruleResult = traitsMatchSegmentRule(rule, segment.key, context); + return ruleResult; + }).length === segment.rules.length; + + return result; +} + +export function traitsMatchSegmentCondition( + condition: SegmentConditionModel, + segmentKey: string, + context?: EvaluationContext +): boolean { + const traits = context?.identity?.traits || {}; + const identityKey = context?.identity?.key || ''; + + if (condition.operator === PERCENTAGE_SPLIT) { + const hashedPercentage = getHashedPercentageForObjIds([segmentKey, identityKey]); + return hashedPercentage <= parseFloat(String(condition.value)); + } + if (!condition.property) { + return false; + } + let traitValue = traits[condition.property]; + + if (condition?.property?.startsWith('$.')) { + traitValue = getContextValue(condition.property, context); + } else { + traitValue = traits[condition.property]; + } + + if (condition.operator === IS_SET) { + return traitValue !== undefined && traitValue !== null; + } else if (condition.operator === IS_NOT_SET) { + return traitValue === undefined || traitValue === null; + } + + if (traitValue !== undefined && traitValue !== null) { + const segmentCondition = new SegmentConditionModel( + condition.operator, + condition.value, + condition.property + ); + return segmentCondition.matchesTraitValue(traitValue); + } + + return false; } function traitsMatchSegmentRule( - identityTraits: TraitModel[], - rule: SegmentRuleModel, - segmentId: number | string, - identityId: number | string + rule: any, + segmentKey: string, + context?: EvaluationContext ): boolean { const matchesConditions = - rule.conditions.length > 0 - ? rule.matchingFunction()( - rule.conditions.map(condition => - traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId) + rule.conditions && rule.conditions.length > 0 + ? evaluateRuleConditions( + rule.type, + rule.conditions.map((condition: any) => + traitsMatchSegmentCondition(condition, segmentKey, context) ) ) : true; - return ( - matchesConditions && - rule.rules.filter(rule => - traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId) - ).length === rule.rules.length - ); + + const matchesSubRules = + rule.rules && rule.rules.length > 0 + ? rule.rules.filter((subRule: any) => + traitsMatchSegmentRule(subRule, segmentKey, context) + ).length === rule.rules.length + : true; + + return matchesConditions && matchesSubRules; } -export function traitsMatchSegmentCondition( - identityTraits: TraitModel[], - condition: SegmentConditionModel, - segmentId: number | string, - identityId: number | string -): boolean { - if (condition.operator == PERCENTAGE_SPLIT) { - var hashedPercentage = getHashedPercentateForObjIds([segmentId, identityId]); - return hashedPercentage <= parseFloat(String(condition.value)); +function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): boolean { + switch (ruleType) { + case 'ALL': + return conditionResults.length === 0 || conditionResults.every(result => result); + case 'ANY': + return conditionResults.length > 0 && conditionResults.some(result => result); + case 'NONE': + return conditionResults.length === 0 || conditionResults.every(result => !result); + default: + return false; } - const traits = identityTraits.filter(t => t.traitKey === condition.property_); - const trait = traits.length > 0 ? traits[0] : undefined; - if (condition.operator === IS_SET) { - return !!trait; - } else if (condition.operator === IS_NOT_SET) { - return trait == undefined; +} + +function getContextValue(jsonPath: string, context?: EvaluationContext): any { + if (!context) return undefined; + + switch (jsonPath) { + case '$.identity.identifier': + return context.identity?.identifier; + case '$.environment.name': + return context.environment?.name; + case '$.environment.key': + return context.environment?.key; + default: + return undefined; } - return trait ? condition.matchesTraitValue(trait.traitValue) : false; } diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 67aca0d..5dfc09f 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -1,6 +1,11 @@ import * as semver from 'semver'; -import { FeatureStateModel } from '../features/models.js'; +import { + FeatureModel, + FeatureStateModel, + MultivariateFeatureOptionModel, + MultivariateFeatureStateValueModel +} from '../features/models.js'; import { getCastingFunction as getCastingFunction } from '../utils/index.js'; import { ALL_RULE, @@ -13,6 +18,9 @@ import { CONDITION_OPERATORS } from './constants.js'; import { isSemver } from './util.js'; +import { EvaluationResultSegments } from '../evaluationResult/models.js'; +import { EvaluationContext } from '../evaluationContext/evaluationContext.types.js'; +import { CONSTANTS } from '../features/constants.js'; export const all = (iterable: Array) => iterable.filter(e => !!e).length === iterable.length; export const any = (iterable: Array) => iterable.filter(e => !!e).length > 0; @@ -57,7 +65,7 @@ export class SegmentConditionModel { operator: string; value: string | null | undefined; - property_: string | null | undefined; + property: string | null | undefined; constructor( operator: string, @@ -66,7 +74,7 @@ export class SegmentConditionModel { ) { this.operator = operator; this.value = value; - this.property_ = property; + this.property = property; } matchesTraitValue(traitValue: any) { @@ -144,4 +152,64 @@ export class SegmentModel { this.id = id; this.name = name; } + + static fromSegmentResult( + segmentResults: EvaluationResultSegments, + evaluationContext: EvaluationContext + ): SegmentModel[] { + const segmentModels: SegmentModel[] = []; + if (!evaluationContext.segments) { + return []; + } + + for (const segmentResult of segmentResults) { + const segmentContext = evaluationContext.segments[segmentResult.key]; + if (segmentContext) { + const segment = new SegmentModel(parseInt(segmentContext.key), segmentContext.name); + segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type)); + segment.featureStates = + segmentContext.overrides?.map(override => { + const feature = new FeatureModel( + parseInt(override.feature_key), + override.name, + override?.variants?.length && override?.variants?.length > 1 + ? CONSTANTS.MULTIVARIATE + : CONSTANTS.STANDARD + ); + + const featureState = new FeatureStateModel( + feature, + override.enabled, + override.priority || 0 + ); + + if (override.value !== undefined) { + featureState.setValue(override.value); + } + + if ( + override.variants && + override?.variants?.length > 1 && + override.variants.length > 0 + ) { + featureState.multivariateFeatureStateValues = override.variants.map( + variant => + new MultivariateFeatureStateValueModel( + new MultivariateFeatureOptionModel( + variant.value, + variant?.id as number + ), + variant.weight as number, + variant.id as number + ) + ); + } + + return featureState; + }) || []; + segmentModels.push(segment); + } + } + return segmentModels; + } } diff --git a/flagsmith-engine/utils/hashing/index.ts b/flagsmith-engine/utils/hashing/index.ts index 72f3f46..1390d13 100644 --- a/flagsmith-engine/utils/hashing/index.ts +++ b/flagsmith-engine/utils/hashing/index.ts @@ -14,7 +14,7 @@ const makeRepeated = (arr: Array, repeats: number) => * @param {} iterations=1 num times to include each id in the generated string to hash * @returns number number between 0 (inclusive) and 100 (exclusive) */ -export function getHashedPercentateForObjIds(objectIds: Array, iterations = 1): number { +export function getHashedPercentageForObjIds(objectIds: Array, iterations = 1): number { let toHash = makeRepeated(objectIds, iterations).join(','); const hashedValue = md5(toHash); const hashedInt = BigInt('0x' + hashedValue); @@ -24,7 +24,7 @@ export function getHashedPercentateForObjIds(objectIds: Array, iterations = /* istanbul ignore next */ if (value === 100) { /* istanbul ignore next */ - return getHashedPercentateForObjIds(objectIds, iterations + 1); + return getHashedPercentageForObjIds(objectIds, iterations + 1); } return value; diff --git a/package-lock.json b/package-lock.json index ffdb63f..f4fd0e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,14 @@ "undici-types": "^6.19.8" }, "devDependencies": { + "@types/jest": "^30.0.0", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "json-schema-to-typescript": "^15.0.4", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", @@ -42,6 +44,39 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", @@ -52,10 +87,11 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -625,6 +661,85 @@ "node": ">=8" } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -673,6 +788,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -949,6 +1071,13 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -956,6 +1085,58 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.16.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", @@ -971,12 +1152,36 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", @@ -1158,6 +1363,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1180,7 +1392,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1201,6 +1414,29 @@ } ] }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1251,6 +1487,23 @@ "node": ">=12" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -1261,6 +1514,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1754,6 +2023,16 @@ "node": ">=18" } }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1780,6 +2059,24 @@ "node": ">=0.8.x" } }, + "node_modules/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -1798,6 +2095,37 @@ "node": ">=6" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -1841,6 +2169,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1890,6 +2246,16 @@ } ] }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1899,6 +2265,29 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1970,6 +2359,169 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-diff": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/loupe": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", @@ -2013,6 +2565,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -2116,6 +2721,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", @@ -2195,6 +2813,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2213,6 +2859,13 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -2341,6 +2994,16 @@ "dev": true, "license": "ISC" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/sonic-boom": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", @@ -2366,6 +3029,19 @@ "node": ">= 10.x" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2468,50 +3144,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/thread-stream": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", @@ -2534,6 +3166,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -2573,6 +3222,19 @@ "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", diff --git a/package.json b/package.json index aa632b6..3274ac6 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,10 @@ "build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}'> build/cjs/package.json", "deploy": "npm i && npm run build && npm publish", "deploy:beta": "npm i && npm run build && npm publish --tag beta", - "prepare": "husky install" + "prepare": "husky install", + "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json", + "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json", + "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types" }, "dependencies": { "pino": "^8.8.0", @@ -65,12 +68,14 @@ "undici-types": "^6.19.8" }, "devDependencies": { + "@types/jest": "^30.0.0", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "json-schema-to-typescript": "^15.0.4", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", diff --git a/sdk/index.ts b/sdk/index.ts index e299288..fbd50a0 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,8 +1,5 @@ import { Dispatcher } from 'undici-types'; -import { - getEnvironmentFeatureStates, - getIdentityFeatureStates -} from '../flagsmith-engine/index.js'; +import { getEvaluationResult } from '../flagsmith-engine/index.js'; import { EnvironmentModel } from '../flagsmith-engine/index.js'; import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'; import { IdentityModel } from '../flagsmith-engine/index.js'; @@ -16,7 +13,6 @@ import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; import { Deferred, generateIdentitiesData, retryFetch } from './utils.js'; import { SegmentModel } from '../flagsmith-engine/index.js'; -import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js'; import { Fetch, FlagsmithCache, @@ -25,6 +21,7 @@ import { TraitConfig } from './types.js'; import { pino, Logger } from 'pino'; +import { getEvaluationContext } from '../flagsmith-engine/evaluationContext/mappers.js'; export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js'; export { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; @@ -278,7 +275,10 @@ export class Flagsmith { })) ); - return getIdentitySegments(environment, identityModel); + const context = getEvaluationContext(environment, identityModel); + const evaluationResult = getEvaluationResult(context); + // DOUBLE CHECK THE IMPLEMENTATION HERE AND IF IT CAN BE REMOVED + return SegmentModel.fromSegmentResult(evaluationResult.segments, context); } private async fetchEnvironment(): Promise { @@ -397,14 +397,14 @@ export class Flagsmith { private async getEnvironmentFlagsFromDocument(): Promise { const environment = await this.getEnvironment(); - const flags = Flags.fromFeatureStateModels({ - featureStates: getEnvironmentFeatureStates(environment), - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler - }); + const context = getEvaluationContext(environment); + const evaluationResult = getEvaluationResult(context); + const flags = Flags.fromEvaluationResult(evaluationResult); + if (!!this.cache) { await this.cache.set('flags', flags); } + return flags; } @@ -422,14 +422,14 @@ export class Flagsmith { })) ); - const featureStates = getIdentityFeatureStates(environment, identityModel); + const context = getEvaluationContext(environment, identityModel); + const evaluationResult = getEvaluationResult(context); - const flags = Flags.fromFeatureStateModels({ - featureStates: featureStates, - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler, - identityID: identityModel.djangoID || identityModel.compositeKey - }); + const flags = Flags.fromEvaluationResult( + evaluationResult, + this.defaultFlagHandler, + this.analyticsProcessor + ); if (!!this.cache) { await this.cache.set(`flags-${identifier}`, flags); diff --git a/sdk/models.ts b/sdk/models.ts index 90cffae..e745f8c 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,9 @@ +import { FeatureContext } from '../flagsmith-engine/evaluationContext/models.js'; +import { EvaluationResult, FlagResult } from '../flagsmith-engine/evaluationResult/types.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; -type FlagValue = string | number | boolean | undefined; +type FlagValue = string | number | boolean | undefined | null; /** * A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}. @@ -56,6 +58,7 @@ export class Flag extends BaseFlag { isDefault?: boolean; featureId: number; featureName: string; + reason?: string; }) { super(params.value, params.enabled, !!params.isDefault); this.featureId = params.featureId; @@ -74,6 +77,15 @@ export class Flag extends BaseFlag { }); } + static fromFlagResult(flagResult: FlagResult): Flag { + return new Flag({ + enabled: flagResult.enabled, + value: flagResult.value ?? undefined, + featureId: Number(flagResult.feature_key), + featureName: flagResult.name + }); + } + static fromAPIFlag(flagData: any): Flag { return new Flag({ enabled: flagData['enabled'], @@ -99,6 +111,28 @@ export class Flags { this.analyticsProcessor = data.analyticsProcessor; } + static fromEvaluationResult( + evaluationResult: EvaluationResult, + defaultFlagHandler?: (v: string) => DefaultFlag, + analyticsProcessor?: AnalyticsProcessor + ): Flags { + const flags: { [key: string]: Flag } = {}; + for (const flag of evaluationResult.flags) { + flags[flag.name] = new Flag({ + enabled: flag.enabled, + value: flag.value ?? null, + featureId: Number(flag.feature_key), + featureName: flag.name, + reason: flag.reason + }); + } + return new Flags({ + flags: flags, + defaultFlagHandler: defaultFlagHandler, + analyticsProcessor: analyticsProcessor + }); + } + static fromFeatureStateModels(data: { featureStates: FeatureStateModel[]; analyticsProcessor?: AnalyticsProcessor; diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index 87d045f..dd22072 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -1,22 +1,15 @@ -import { getIdentityFeatureStates } from '../../../flagsmith-engine/index.js'; -import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js'; -import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js'; -import { IdentityModel } from '../../../flagsmith-engine/identities/models.js'; -import { buildIdentityModel } from '../../../flagsmith-engine/identities/util.js'; +import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; +import { Flags } from '../../../sdk/models.js'; import * as testData from '../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; function extractTestCases(data: any): { - environment: EnvironmentModel; - identity: IdentityModel; response: any; + context: EvaluationContext; }[] { - const environmentModel = buildEnvironmentModel(data['environment']); - const test_data = data['identities_and_responses'].map((test_case: any) => { - const identity = buildIdentityModel(test_case['identity']); - + const test_data = data['test_cases'].map((test_case: any) => { return { - environment: environmentModel, - identity: identity, + context: test_case['context'], response: test_case['response'] }; }); @@ -26,10 +19,11 @@ function extractTestCases(data: any): { test('Test Engine', () => { const testCases = extractTestCases(testData); for (const testCase of testCases) { - const engine_response = getIdentityFeatureStates(testCase.environment, testCase.identity); - const sortedEngineFlags = engine_response.sort((a, b) => - a.feature.name > b.feature.name ? 1 : -1 - ); + const engine_response = getEvaluationResult(testCase.context); + const flags = Flags.fromEvaluationResult(engine_response); + const sortedEngineFlags = flags + .allFlags() + .sort((a, b) => (a.featureName > b.featureName ? 1 : -1)); const sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) => a.feature.name > b.feature.name ? 1 : -1 ); @@ -37,9 +31,7 @@ test('Test Engine', () => { expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); for (let i = 0; i < sortedEngineFlags.length; i++) { - expect(sortedEngineFlags[i].getValue(testCase.identity.djangoID)).toBe( - sortedAPIFlags[i]['feature_state_value'] - ); + expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i]['feature_state_value']); expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i]['enabled']); } } diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 933f2ba..5e7c413 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 933f2ba7aa6430797afc2d053530cfd005b461f6 +Subproject commit 5e7c4139c59e529301f7dc8f784e991f1c8840fb diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 15b27d1..0175155 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -1,9 +1,4 @@ -import { - getEnvironmentFeatureState, - getEnvironmentFeatureStates, - getIdentityFeatureState, - getIdentityFeatureStates -} from '../../../flagsmith-engine/index.js'; +import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js'; import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js'; import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js'; @@ -11,102 +6,138 @@ import { environment, environmentWithSegmentOverride, feature1, - getEnvironmentFeatureStateForFeature, - getEnvironmentFeatureStateForFeatureByName, identity, identityInSegment, segmentConditionProperty, segmentConditionStringValue } from './utils.js'; +import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; -test('test_identity_get_feature_state_without_any_override', () => { - const feature_state = getIdentityFeatureState(environment(), identity(), feature1().name); +test('test_get_evaluation_result_without_any_override', () => { + const context = getEvaluationContext(environment(), identity()); + const result = getEvaluationResult(context); - expect(feature_state.feature).toStrictEqual(feature1()); + const flag = result.flags.find(f => f.name === feature1().name); + expect(flag).toBeDefined(); + expect(flag?.name).toBe(feature1().name); + expect(flag?.feature_key).toBe(feature1().id.toString()); + expect(flag?.reason).toBe('DEFAULT'); }); -test('test_identity_get_feature_state_without_any_override_no_fs', () => { - expect(() => { - getIdentityFeatureState(environment(), identity(), 'nonExistentName'); - }).toThrowError('Feature State Not Found'); -}); +// CHECK IF THIS TEST IS STILL NEEDED +// test('test_identity_get_feature_state_from_contextwithout_any_override_no_fs', () => { +// expect(() => { +// const context = getEvaluationContext(environment(), identity()); +// getIdentityFeatureStateFromContext(context, 'nonExistentName'); +// }).toThrowError('Feature State Not Found'); +// }); -test('test_identity_get_all_feature_states_no_segments', () => { +test('test_get_evaluation_result_with_identity_override_and_no_segment_override', () => { const env = environment(); const ident = identity(); const overridden_feature = new FeatureModel(3, 'overridden_feature', CONSTANTS.STANDARD); env.featureStates.push(new FeatureStateModel(overridden_feature, false, 3)); - ident.identityFeatures = [new FeatureStateModel(overridden_feature, true, 4)]; - const featureStates = getIdentityFeatureStates(env, ident); + const context = getEvaluationContext(env, ident); + const result = getEvaluationResult(context); - expect(featureStates.length).toBe(3); - for (const featuresState of featureStates) { - const environmentFeatureState = getEnvironmentFeatureStateForFeature( - env, - featuresState.feature + expect(result.flags.length).toBe(3); + + for (const flag of result.flags) { + const environmentFeature = Object.values(context.features || {}).find( + f => f.name === flag.name ); - const expected = - environmentFeatureState?.feature == overridden_feature - ? true - : environmentFeatureState?.enabled; - expect(featuresState.enabled).toBe(expected); - } -}); -test('test_identity_get_all_feature_states_with_traits', () => { - const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); + const expected = flag.name === 'overridden_feature' ? true : environmentFeature?.enabled; - const featureStates = getIdentityFeatureStates( - environmentWithSegmentOverride(), - identityInSegment(), - [trait_models] - ); - expect(featureStates[0].getValue()).toBe('segment_override'); + expect(flag.enabled).toBe(expected); + expect(flag.reason).toBe( + flag.name === 'overridden_feature' ? 'IDENTITY_OVERRIDE' : 'DEFAULT' + ); + } }); -test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => { +test('test_identity_get_all_feature_states_with_traits', () => { const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); - const env = environmentWithSegmentOverride(); - env.project.hideDisabledFlags = true; + const context = getEvaluationContext(environmentWithSegmentOverride(), identityInSegment(), [ + trait_models + ]); - const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]); - expect(featureStates.length).toBe(0); -}); - -test('test_environment_get_all_feature_states', () => { - const env = environment(); - const featureStates = getEnvironmentFeatureStates(env); + const result = getEvaluationResult(context); - expect(featureStates).toBe(env.featureStates); + const overriddenFlag = result.flags.find(f => f.value === 'segment_override'); + expect(overriddenFlag).toBeDefined(); + expect(overriddenFlag?.value).toBe('segment_override'); + expect(overriddenFlag?.reason).toEqual('TARGETING_MATCH; segment=test name'); }); -test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => { - const env = environment(); +// TO CONFIRM ITS REMOVED +// test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => { +// const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); - env.project.hideDisabledFlags = true; +// const env = environmentWithSegmentOverride(); - const featureStates = getEnvironmentFeatureStates(env); +// const context = getEvaluationContext(env, identityInSegment(), [trait_models]); +// const result = getEvaluationResult(context, true); - expect(featureStates).not.toBe(env.featureStates); - for (const fs of featureStates) { - expect(fs.enabled).toBe(true); - } -}); +// expect(result.flags.length).toBe(0); +// }); -test('test_environment_get_feature_state', () => { +test('test_environment_get_all_feature_states', () => { const env = environment(); - const feature = feature1(); - const featureState = getEnvironmentFeatureState(env, feature.name); + const context = getEvaluationContext(env); + const result = getEvaluationResult(context); - expect(featureState.feature).toStrictEqual(feature); -}); + expect(result.flags.length).toBe(Object.keys(context.features || {}).length); -test('test_environment_get_feature_state_raises_feature_state_not_found', () => { - expect(() => { - getEnvironmentFeatureState(environment(), 'not_a_feature_name'); - }).toThrowError('Feature State Not Found'); + result.flags.forEach(flag => { + expect(flag.reason).toBe('DEFAULT'); + }); + + for (const flag of result.flags) { + const envFeature = Object.values(context.features || {}).find(f => f.name === flag.name); + expect(flag.enabled).toBe(envFeature?.enabled); + expect(flag.value).toBe(envFeature?.value); + } }); +// CONFIRM hide_disabled_flags is removed in local evaluation +// test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => { +// // One feature is disabled this environment +// const env = environment(); +// const context = getEvaluationContext(env); +// const result = getEvaluationResult(context, true); + +// expect(result.flags.length).toBe(1); + +// result.flags.forEach(flag => { +// expect(flag.reason).toBe('DEFAULT'); +// }); + +// for (const flag of result.flags) { +// const envFeature = Object.values(context.features || {}).find(f => f.name === flag.name); +// expect(flag.enabled).toBe(envFeature?.enabled); +// expect(flag.value).toBe(envFeature?.value); +// } +// }); + +// Check if this test is still needed +// test('test_environment_get_feature_state', () => { +// const env = environment(); +// const feature = feature1(); +// const context = getEvaluationContext(env, identity()); +// const featureState = getEnvironmentFeatureStateFromContext(context, feature.name); + +// expect(featureState.name).toStrictEqual(feature.name); +// }); + +// Check if this test is still needed +// test('test_environment_get_feature_state_raises_feature_state_not_found', () => { +// const context = getEvaluationContext(environment(), identity()); +// const result = getEvaluationResult(context); +// expect(() => { +// getEnvironmentFeatureStateFromContext(context, 'not_a_feature_name'); +// }).toThrowError('Feature State Not Found'); +// }); diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 1a73eec..77356b3 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -11,11 +11,13 @@ import { import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js'; import { environment } from '../utils.js'; import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js'; -import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getEvaluationContext } from '../../../../flagsmith-engine/evaluationContext/mappers.js'; +import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; // todo: work out how to implement this in a test function or before hook vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({ - getHashedPercentateForObjIds: vi.fn(() => 1) + getHashedPercentageForObjIds: vi.fn(() => 1) })); let traitExistenceTestCases: [ @@ -48,8 +50,27 @@ let traitExistenceTestCases: [ test('test_traits_match_segment_condition_for_trait_existence_operators', () => { for (const testCase of traitExistenceTestCases) { const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase; - let segmentModel = new SegmentConditionModel(operator, conditionValue, conditionProperty); - expect(traitsMatchSegmentCondition(traits, segmentModel, 'any', 'any')).toBe( + let segmentConditionModel = new SegmentConditionModel( + operator, + conditionValue, + conditionProperty + ); + const traitsMap = traits.reduce((acc, trait) => { + acc[trait.traitKey] = trait.traitValue; + return acc; + }, {}); + const context: EvaluationContext = { + environment: { + key: 'any', + name: 'any' + }, + identity: { + traits: traitsMap, + key: 'any', + identifier: 'any' + } + }; + expect(traitsMatchSegmentCondition(segmentConditionModel, 'any', context)).toBe( expectedResult ); } @@ -84,13 +105,17 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen feature_states: [] }; const segmentModel = buildSegmentModel(segmentDefinition); + const environmentModel = environment(); + environmentModel.project.segments = [segmentModel]; + const context = getEvaluationContext(environmentModel, identityModel); - var result = evaluateIdentityInSegment(identityModel, segmentModel); + const segmentContext = context.segments![1]; + var result = evaluateIdentityInSegment(segmentContext, context); expect(result).toBe(true); - expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1); - expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([ - segmentModel.id, - identityModel.djangoID + expect(getHashedPercentageForObjIds).toHaveBeenCalledTimes(1); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith([ + segmentContext.key, + context.identity!.key ]); }); diff --git a/tests/engine/unit/utils.ts b/tests/engine/unit/utils.ts index cdb73b2..4e89fca 100644 --- a/tests/engine/unit/utils.ts +++ b/tests/engine/unit/utils.ts @@ -20,7 +20,7 @@ export function segmentCondition() { } export function traitMatchingSegment() { - return new TraitModel(segmentCondition().property_ as string, segmentCondition().value); + return new TraitModel(segmentCondition().property as string, segmentCondition().value); } export function organisation() { diff --git a/tests/engine/unit/utils/utils.test.ts b/tests/engine/unit/utils/utils.test.ts index 041adfc..15a1a30 100644 --- a/tests/engine/unit/utils/utils.test.ts +++ b/tests/engine/unit/utils/utils.test.ts @@ -1,11 +1,11 @@ import { randomUUID as uuidv4 } from 'node:crypto'; -import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; describe('getHashedPercentageForObjIds', () => { it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])( 'returns x where 0 <= x < 100', (objIds: (string | number)[]) => { - let result = getHashedPercentateForObjIds(objIds); + let result = getHashedPercentageForObjIds(objIds); expect(result).toBeLessThan(100); expect(result).toBeGreaterThanOrEqual(0); } @@ -14,15 +14,15 @@ describe('getHashedPercentageForObjIds', () => { it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])( 'returns the same value each time', (objIds: (string | number)[]) => { - let resultOne = getHashedPercentateForObjIds(objIds); - let resultTwo = getHashedPercentateForObjIds(objIds); + let resultOne = getHashedPercentageForObjIds(objIds); + let resultTwo = getHashedPercentageForObjIds(objIds); expect(resultOne).toEqual(resultTwo); } ); it('is unique for different object ids', () => { - let resultOne = getHashedPercentateForObjIds([14, 106]); - let resultTwo = getHashedPercentateForObjIds([53, 200]); + let resultOne = getHashedPercentageForObjIds([14, 106]); + let resultTwo = getHashedPercentageForObjIds([53, 200]); expect(resultOne).not.toEqual(resultTwo); }); @@ -40,7 +40,7 @@ describe('getHashedPercentageForObjIds', () => { ); // When - let values = objectIdPairs.map(objIds => getHashedPercentateForObjIds(objIds)); + let values = objectIdPairs.map(objIds => getHashedPercentageForObjIds(objIds)); // Then for (let i = 0; i++; i < numTestBuckets) { From 3503df2b73c0dd3d22a4b2939d7b19e7688fd955 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 17:45:58 +0200 Subject: [PATCH 02/25] feat: updated-pre-commit-to-generate-types-before-tests --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 938cbdb..a0a1fde 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,4 +3,5 @@ npm run lint git add ./flagsmith-engine ./sdk ./tests ./index.ts ./.github +npm run generate-engine-types npm run test \ No newline at end of file From 12298892b7b6b9e2c15b3f048d115d2a3ab182b8 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 17:46:30 +0200 Subject: [PATCH 03/25] feat: types From 768440c384ccd024993e612def2067b9980be751 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 17:47:12 +0200 Subject: [PATCH 04/25] feat: generate-types-before-lint --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index a0a1fde..c221482 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,7 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +npm run generate-engine-types npm run lint git add ./flagsmith-engine ./sdk ./tests ./index.ts ./.github -npm run generate-engine-types npm run test \ No newline at end of file From ffcb468a94c924e77075a903fb51019e436826e2 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 18:09:30 +0200 Subject: [PATCH 05/25] feat: improved-typings --- flagsmith-engine/evaluationContext/mappers.ts | 3 +- flagsmith-engine/evaluationResult/types.ts | 290 ------------------ flagsmith-engine/features/types.ts | 5 + flagsmith-engine/index.ts | 13 +- flagsmith-engine/segments/constants.ts | 1 + flagsmith-engine/segments/evaluators.ts | 18 +- sdk/models.ts | 2 +- tests/engine/unit/engine.test.ts | 13 +- 8 files changed, 37 insertions(+), 308 deletions(-) delete mode 100644 flagsmith-engine/evaluationResult/types.ts create mode 100644 flagsmith-engine/features/types.ts diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts index 4ce5c7e..247a90c 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -9,6 +9,7 @@ import { import { EnvironmentModel } from '../environments/models.js'; import { IdentityModel } from '../identities/models.js'; import { TraitModel } from '../identities/traits/models.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../segments/constants.js'; export function getEvaluationContext( environment: EnvironmentModel, @@ -172,7 +173,7 @@ function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { segments[segmentKey] = { key: segmentKey, - name: 'identity_overrides', + name: IDENTITY_OVERRIDE_SEGMENT_NAME, rules: [ { type: 'ALL', diff --git a/flagsmith-engine/evaluationResult/types.ts b/flagsmith-engine/evaluationResult/types.ts deleted file mode 100644 index fc0df3b..0000000 --- a/flagsmith-engine/evaluationResult/types.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* eslint-disable */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -/** - * An environment's unique identifier. - */ -export type Key = string; -/** - * An environment's human-readable name. - */ -export type Name = string; -/** - * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. - */ -export type Identifier = string; -/** - * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. - */ -export type Key1 = string; -/** - * Key used for % split segmentation. - */ -export type Key2 = string; -/** - * The name of the segment. - */ -export type Name1 = string; -/** - * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. - */ -export type Type = 'ALL' | 'ANY' | 'NONE'; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator = - | 'EQUAL' - | 'GREATER_THAN' - | 'LESS_THAN' - | 'LESS_THAN_INCLUSIVE' - | 'CONTAINS' - | 'GREATER_THAN_INCLUSIVE' - | 'NOT_CONTAINS' - | 'NOT_EQUAL' - | 'REGEX' - | 'PERCENTAGE_SPLIT' - | 'MODULO' - | 'IS_SET' - | 'IS_NOT_SET' - | 'IN'; -/** - * The value to compare against the trait or context value. - */ -export type Value = string; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property1 = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator1 = 'IN'; -/** - * The values to compare against the trait or context value. - */ -export type Value1 = string[]; -/** - * Conditions that must be met for the rule to apply. - */ -export type Conditions = (SegmentCondition | InSegmentCondition)[]; -/** - * Sub-rules nested within the segment rule. - */ -export type SubRules = SegmentRule[]; -/** - * Rules that define the segment. - */ -export type Rules = SegmentRule[]; -/** - * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. - */ -export type Key3 = string; -/** - * Unique feature identifier. - */ -export type FeatureKey = string; -/** - * Feature name. - */ -export type Name2 = string; -/** - * Indicates whether the feature is enabled in the environment. - */ -export type Enabled = boolean; -/** - * A default environment value for the feature. If the feature is multivariate, this will be the control value. - */ -export type Value2 = string | number | boolean | null; -/** - * The value of the feature. - */ -export type Value3 = string | number | boolean | null; -/** - * The weight of the feature value variant, as a percentage number (i.e. 100.0). - */ -export type Weight = number; -/** - * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. - */ -export type Variants = FeatureValue[]; -/** - * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. - */ -export type Priority = number; -/** - * Feature overrides for the segment. - */ -export type Overrides = FeatureContext[]; -/** - * Unique feature identifier. - */ -export type FeatureKey1 = string; -/** - * Feature name. - */ -export type Name3 = string; -/** - * Indicates if the feature flag is enabled. - */ -export type Enabled1 = boolean; -/** - * Feature flag value. - */ -export type Value4 = string | number | boolean | null; -/** - * Reason for the feature flag evaluation. - */ -export type Reason = string; -/** - * List of feature flags evaluated for the context. - */ -export type Flags = FlagResult[]; -/** - * Unique segment identifier. - */ -export type Key4 = string; -/** - * Segment name. - */ -export type Name4 = string; -/** - * List of segments which the provided context belongs to. - */ -export type Segments1 = SegmentResult[]; - -/** - * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. - */ -export interface EvaluationResult { - context: EvaluationContext; - flags: Flags; - segments: Segments1; - [k: string]: unknown; -} -/** - * A context object containing the necessary information to evaluate Flagsmith feature flags. - */ -export interface EvaluationContext { - environment: EnvironmentContext; - /** - * Identity context used for identity-based evaluation. - */ - identity?: IdentityContext | null; - segments?: Segments; - features?: Features; - [k: string]: unknown; -} -/** - * Environment context required for evaluation. - */ -export interface EnvironmentContext { - key: Key; - name: Name; - [k: string]: unknown; -} -/** - * Represents an identity context for feature flag evaluation. - */ -export interface IdentityContext { - identifier: Identifier; - key: Key1; - traits?: Traits; - [k: string]: unknown; -} -/** - * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. - */ -export interface Traits { - [k: string]: string | number | boolean | null; -} -/** - * Segments applicable to the evaluation context. - */ -export interface Segments { - [k: string]: SegmentContext; -} -/** - * Represents a segment context for feature flag evaluation. - */ -export interface SegmentContext { - key: Key2; - name: Name1; - rules: Rules; - overrides?: Overrides; - [k: string]: unknown; -} -/** - * Represents a rule within a segment for feature flag evaluation. - */ -export interface SegmentRule { - type: Type; - conditions?: Conditions; - rules?: SubRules; - [k: string]: unknown; -} -/** - * Represents a condition within a segment rule for feature flag evaluation. - */ -export interface SegmentCondition { - property: Property; - operator: Operator; - value: Value; - [k: string]: unknown; -} -/** - * Represents an IN condition within a segment rule for feature flag evaluation. - */ -export interface InSegmentCondition { - property: Property1; - operator: Operator1; - value: Value1; - [k: string]: unknown; -} -/** - * Represents a feature context for feature flag evaluation. - */ -export interface FeatureContext { - key: Key3; - feature_key: FeatureKey; - name: Name2; - enabled: Enabled; - value: Value2; - variants?: Variants; - priority?: Priority; - [k: string]: unknown; -} -/** - * Represents a multivariate value for a feature flag. - */ -export interface FeatureValue { - value: Value3; - weight: Weight; - [k: string]: unknown; -} -/** - * Features to be evaluated in the context. - */ -export interface Features { - [k: string]: FeatureContext; -} -export interface FlagResult { - feature_key: FeatureKey1; - name: Name3; - enabled: Enabled1; - value?: Value4; - reason?: Reason; - [k: string]: unknown; -} -export interface SegmentResult { - key: Key4; - name: Name4; - [k: string]: unknown; -} diff --git a/flagsmith-engine/features/types.ts b/flagsmith-engine/features/types.ts new file mode 100644 index 0000000..8417788 --- /dev/null +++ b/flagsmith-engine/features/types.ts @@ -0,0 +1,5 @@ +export enum TARGETING_REASONS { + DEFAULT = 'DEFAULT', + IDENTITY_OVERRIDE = 'IDENTITY_OVERRIDE', + TARGETING_MATCH = 'TARGETING_MATCH' +} diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 0db0319..51ca419 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -2,6 +2,8 @@ import { EvaluationContext, FeatureContext } from './evaluationContext/models.js import { getIdentitySegments } from './segments/evaluators.js'; import { EvaluationResult, EvaluationResultFlags } from './evaluationResult/models.js'; import { evaluateFeatureValue } from './features/util.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from './segments/constants.js'; +import { TARGETING_REASONS } from './features/types.js'; export { EnvironmentModel } from './environments/models.js'; export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; @@ -69,12 +71,11 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul return { context, flags, segments }; } -const getTargetingMatchReason = (segmentOverride: segmentOverride, segmentName: string) => { +const getTargetingMatchReason = (segmentOverride: segmentOverride) => { if (segmentOverride) { - // TURN INTO CONSTANT - return segmentOverride.segmentName === 'identity_overrides' - ? 'IDENTITY_OVERRIDE' - : `TARGETING_MATCH; segment=${segmentOverride.segmentName}`; + return segmentOverride.segmentName === IDENTITY_OVERRIDE_SEGMENT_NAME + ? TARGETING_REASONS.IDENTITY_OVERRIDE + : `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}`; } - return `DEFAULT`; + return TARGETING_REASONS.DEFAULT; }; diff --git a/flagsmith-engine/segments/constants.ts b/flagsmith-engine/segments/constants.ts index d2a3e9b..fad1660 100644 --- a/flagsmith-engine/segments/constants.ts +++ b/flagsmith-engine/segments/constants.ts @@ -4,6 +4,7 @@ export const ANY_RULE = 'ANY'; export const NONE_RULE = 'NONE'; export const RULE_TYPES = [ALL_RULE, ANY_RULE, NONE_RULE]; +export const IDENTITY_OVERRIDE_SEGMENT_NAME = 'identity_overrides'; // Segment Condition Operators export const EQUAL = 'EQUAL'; diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index e2638ce..671dac5 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,4 +1,10 @@ -import { EvaluationContext, IdentityContext, SegmentContext } from '../evaluationContext/models.js'; +import { + EvaluationContext, + IdentityContext, + SegmentCondition, + SegmentContext, + SegmentRule +} from '../evaluationContext/models.js'; import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; import { SegmentConditionModel } from './models.js'; import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; @@ -29,7 +35,7 @@ export function evaluateIdentityInSegment( } export function traitsMatchSegmentCondition( - condition: SegmentConditionModel, + condition: SegmentCondition, segmentKey: string, context?: EvaluationContext ): boolean { @@ -60,7 +66,7 @@ export function traitsMatchSegmentCondition( if (traitValue !== undefined && traitValue !== null) { const segmentCondition = new SegmentConditionModel( condition.operator, - condition.value, + condition.value as string, condition.property ); return segmentCondition.matchesTraitValue(traitValue); @@ -70,7 +76,7 @@ export function traitsMatchSegmentCondition( } function traitsMatchSegmentRule( - rule: any, + rule: SegmentRule, segmentKey: string, context?: EvaluationContext ): boolean { @@ -78,7 +84,7 @@ function traitsMatchSegmentRule( rule.conditions && rule.conditions.length > 0 ? evaluateRuleConditions( rule.type, - rule.conditions.map((condition: any) => + rule.conditions.map((condition: SegmentCondition) => traitsMatchSegmentCondition(condition, segmentKey, context) ) ) @@ -86,7 +92,7 @@ function traitsMatchSegmentRule( const matchesSubRules = rule.rules && rule.rules.length > 0 - ? rule.rules.filter((subRule: any) => + ? rule.rules.filter((subRule: SegmentRule) => traitsMatchSegmentRule(subRule, segmentKey, context) ).length === rule.rules.length : true; diff --git a/sdk/models.ts b/sdk/models.ts index e745f8c..a987df1 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -80,7 +80,7 @@ export class Flag extends BaseFlag { static fromFlagResult(flagResult: FlagResult): Flag { return new Flag({ enabled: flagResult.enabled, - value: flagResult.value ?? undefined, + value: flagResult.value ?? null, featureId: Number(flagResult.feature_key), featureName: flagResult.name }); diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 0175155..70647c9 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -12,6 +12,7 @@ import { segmentConditionStringValue } from './utils.js'; import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; +import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; test('test_get_evaluation_result_without_any_override', () => { const context = getEvaluationContext(environment(), identity()); @@ -21,7 +22,7 @@ test('test_get_evaluation_result_without_any_override', () => { expect(flag).toBeDefined(); expect(flag?.name).toBe(feature1().name); expect(flag?.feature_key).toBe(feature1().id.toString()); - expect(flag?.reason).toBe('DEFAULT'); + expect(flag?.reason).toBe(TARGETING_REASONS.DEFAULT); }); // CHECK IF THIS TEST IS STILL NEEDED @@ -54,7 +55,9 @@ test('test_get_evaluation_result_with_identity_override_and_no_segment_override' expect(flag.enabled).toBe(expected); expect(flag.reason).toBe( - flag.name === 'overridden_feature' ? 'IDENTITY_OVERRIDE' : 'DEFAULT' + flag.name === 'overridden_feature' + ? TARGETING_REASONS.IDENTITY_OVERRIDE + : TARGETING_REASONS.DEFAULT ); } }); @@ -71,7 +74,9 @@ test('test_identity_get_all_feature_states_with_traits', () => { const overriddenFlag = result.flags.find(f => f.value === 'segment_override'); expect(overriddenFlag).toBeDefined(); expect(overriddenFlag?.value).toBe('segment_override'); - expect(overriddenFlag?.reason).toEqual('TARGETING_MATCH; segment=test name'); + expect(overriddenFlag?.reason).toEqual( + `${TARGETING_REASONS.TARGETING_MATCH}; segment=test name` + ); }); // TO CONFIRM ITS REMOVED @@ -94,7 +99,7 @@ test('test_environment_get_all_feature_states', () => { expect(result.flags.length).toBe(Object.keys(context.features || {}).length); result.flags.forEach(flag => { - expect(flag.reason).toBe('DEFAULT'); + expect(flag.reason).toBe(TARGETING_REASONS.DEFAULT); }); for (const flag of result.flags) { From 94e13e8272fa13809fcce80674b5fdec8587623a Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 18:14:53 +0200 Subject: [PATCH 06/25] feat: improved-typings-bis --- flagsmith-engine/index.ts | 2 +- flagsmith-engine/segments/evaluators.ts | 9 ++------- sdk/models.ts | 6 ++++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 51ca419..ae74714 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -51,7 +51,7 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul for (const feature of Object.values(context.features || {})) { const segmentOverride = segmentOverrides[feature.feature_key]; const finalFeature = segmentOverride ? segmentOverride.feature : feature; - const reason = getTargetingMatchReason(segmentOverride, segmentOverride?.segmentName); + const reason = getTargetingMatchReason(segmentOverride); const hasOverride = !!segmentOverride; flags.push({ diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 671dac5..3a25079 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,6 +1,5 @@ import { EvaluationContext, - IdentityContext, SegmentCondition, SegmentContext, SegmentRule @@ -8,13 +7,9 @@ import { import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; import { SegmentConditionModel } from './models.js'; import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; -import { EvaluationResult } from '../evaluationResult/models.js'; - -export function getIdentitySegments(context: EvaluationContext): EvaluationResult['segments'] { - if (!context.identity || !context.segments) { - return []; - } +export function getIdentitySegments(context: EvaluationContext): SegmentContext[] { + if (!context.identity || !context.segments) return []; return Object.values(context.segments).filter(segment => evaluateIdentityInSegment(segment, context) ); diff --git a/sdk/models.ts b/sdk/models.ts index a987df1..d9896cf 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,5 +1,7 @@ -import { FeatureContext } from '../flagsmith-engine/evaluationContext/models.js'; -import { EvaluationResult, FlagResult } from '../flagsmith-engine/evaluationResult/types.js'; +import { + EvaluationResult, + FlagResult +} from '../flagsmith-engine/evaluationResult/evaluationResult.types.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; From 55de0f6d96f7280395c1b653819b38f63f25d1cf Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 11:57:28 +0200 Subject: [PATCH 07/25] feat: improved-engine-readibility --- flagsmith-engine/evaluationContext/mappers.ts | 12 -- flagsmith-engine/index.ts | 131 +++++++++++++----- sdk/index.ts | 15 +- 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts index 247a90c..0584e1c 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -141,18 +141,6 @@ export function addIdentityToEvaluationContext( }; } -function mapRawSegmentRule(rule: any): any { - return { - type: rule.type, - conditions: rule.conditions?.map((condition: any) => ({ - property: condition.property, - operator: condition.operator, - value: condition.value - })), - rules: rule.rules?.map((subRule: any) => mapRawSegmentRule(subRule)) - }; -} - function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { const segments: Segments = {}; diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index ae74714..9af3522 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -8,51 +8,109 @@ export { EnvironmentModel } from './environments/models.js'; export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; export { SegmentModel } from './segments/models.js'; -// 1. Mappers => Env/identities/segments => EvaluationContext -// 2. One entrypoint => getEvaluationResult -// 3. All these must be disappear type segmentOverride = { feature: FeatureContext; segmentName: string; }; +/** + * Evaluates flags and segments for the given context. + * + * This is the main entry point for the evaluation engine. It processes segments, + * applies feature overrides based on segment priority, and returns the final flag states with + * evaluation reasons. + * + * @param context - EvaluationContext containing environment, identity, and segment data + * @returns EvaluationResult with flags, segments, and original context + */ export function getEvaluationResult(context: EvaluationContext): EvaluationResult { - const segments: EvaluationResult['segments'] = []; + const { segments, segmentOverrides } = evaluateSegments(context); + const flags = evaluateFeatures(context, segmentOverrides); + + // Not sure if we need this - Keeping till confirmed hidedisabledflags is remote evaluation only + // const filteredFlags = hideDisabledFlags ? flags.filter(flag => flag.enabled) : flags; + + return { context, flags, segments }; +} + +/** + * Evaluates which segments the identity belongs to and collects feature overrides. + * + * @param context - EvaluationContext containing identity and segment definitions + * @returns Object containing segments the identity belongs to and any feature overrides + */ +function evaluateSegments(context: EvaluationContext): { + segments: EvaluationResult['segments']; + segmentOverrides: Record; +} { + if (!context.identity || !context.segments) { + return { segments: [], segmentOverrides: {} }; + } + const identitySegments = getIdentitySegments(context); + + const segments = identitySegments.map(segment => ({ + key: segment.key, + name: segment.name + })); + const segmentOverrides = processSegmentOverrides(identitySegments); + + return { segments, segmentOverrides }; +} + +/** + * Processes feature overrides from segments, applying priority rules. + * + * When multiple segments override the same feature, the segment with + * higher priority (lower numeric value) takes precedence. + * + * @param identitySegments - Segments that the identity belongs to + * @returns Map of feature keys to their highest-priority segment overrides + */ +function processSegmentOverrides(identitySegments: any[]): Record { const segmentOverrides: Record = {}; - const DEFAULT_PRIORITY = Infinity; - - if (context.identity && context.segments) { - const identitySegments = getIdentitySegments(context); - - for (const segment of identitySegments) { - segments.push({ key: segment.key, name: segment.name }); - - if (segment.overrides) { - const overridesList = Array.isArray(segment.overrides) ? segment.overrides : []; - for (const override of overridesList) { - const currentOverride = segmentOverrides[override.feature_key]; - if ( - !currentOverride || - (override.priority ?? DEFAULT_PRIORITY) < - (currentOverride.feature.priority ?? DEFAULT_PRIORITY) - ) { - segmentOverrides[override.feature_key] = { - feature: override, - segmentName: segment.name - }; - } - } + + for (const segment of identitySegments) { + if (!segment.overrides) continue; + + const overridesList = Array.isArray(segment.overrides) ? segment.overrides : []; + + for (const override of overridesList) { + if (shouldApplyOverride(override, segmentOverrides)) { + segmentOverrides[override.feature_key] = { + feature: override, + segmentName: segment.name + }; } } } + return segmentOverrides; +} + +/** + * Evaluates all features in the context, applying segment overrides where applicable. + * For each feature: + * - Checks if a segment override exists + * - Uses override values if present, otherwise evaluates the base feature + * - Determines appropriate evaluation reason + * - Handles multivariate evaluation for features without overrides + * + * @param context - EvaluationContext containing features and identity + * @param segmentOverrides - Map of feature keys to their segment overrides + * @returns EvaluationResultFlags containing evaluated flag results + */ +function evaluateFeatures( + context: EvaluationContext, + segmentOverrides: Record +): EvaluationResultFlags { const flags: EvaluationResultFlags = []; + for (const feature of Object.values(context.features || {})) { const segmentOverride = segmentOverrides[feature.feature_key]; const finalFeature = segmentOverride ? segmentOverride.feature : feature; - const reason = getTargetingMatchReason(segmentOverride); const hasOverride = !!segmentOverride; + const reason = getTargetingMatchReason(segmentOverride); flags.push({ feature_key: finalFeature.feature_key, @@ -65,10 +123,21 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul }); } - // Not sure if we need this - Keeping till confirmed hidedisabledflags is remote evaluation only - // const filteredFlags = hideDisabledFlags ? flags.filter(flag => flag.enabled) : flags; + return flags; +} - return { context, flags, segments }; +function shouldApplyOverride( + override: any, + existingOverrides: Record +): boolean { + const currentOverride = existingOverrides[override.feature_key]; + return ( + !currentOverride || isHigherPriority(override.priority, currentOverride.feature.priority) + ); +} + +function isHigherPriority(priorityA: number | undefined, priorityB: number | undefined): boolean { + return (priorityA ?? Infinity) < (priorityB ?? Infinity); } const getTargetingMatchReason = (segmentOverride: segmentOverride) => { diff --git a/sdk/index.ts b/sdk/index.ts index fbd50a0..5d58ac5 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,9 +1,6 @@ import { Dispatcher } from 'undici-types'; -import { getEvaluationResult } from '../flagsmith-engine/index.js'; -import { EnvironmentModel } from '../flagsmith-engine/index.js'; + import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'; -import { IdentityModel } from '../flagsmith-engine/index.js'; -import { TraitModel } from '../flagsmith-engine/index.js'; import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js'; import { BaseOfflineHandler } from './offline_handlers.js'; @@ -12,7 +9,13 @@ import { FlagsmithAPIError } from './errors.js'; import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; import { Deferred, generateIdentitiesData, retryFetch } from './utils.js'; -import { SegmentModel } from '../flagsmith-engine/index.js'; +import { + SegmentModel, + EnvironmentModel, + IdentityModel, + TraitModel, + getEvaluationResult +} from '../flagsmith-engine/index.js'; import { Fetch, FlagsmithCache, @@ -277,7 +280,7 @@ export class Flagsmith { const context = getEvaluationContext(environment, identityModel); const evaluationResult = getEvaluationResult(context); - // DOUBLE CHECK THE IMPLEMENTATION HERE AND IF IT CAN BE REMOVED + return SegmentModel.fromSegmentResult(evaluationResult.segments, context); } From 9817c0e7e0eb546211dfd7dd245d2913b7c91b2b Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 12:19:10 +0200 Subject: [PATCH 08/25] feat: added-unit-tests --- flagsmith-engine/index.ts | 27 +-- tests/engine/unit/engine.test.ts | 279 ++++++++++++++++++++++++++++++- 2 files changed, 294 insertions(+), 12 deletions(-) diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 9af3522..fbb278f 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -9,11 +9,13 @@ export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; export { SegmentModel } from './segments/models.js'; -type segmentOverride = { +type SegmentOverride = { feature: FeatureContext; segmentName: string; }; +export type SegmentOverrides = Record; + /** * Evaluates flags and segments for the given context. * @@ -40,9 +42,9 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul * @param context - EvaluationContext containing identity and segment definitions * @returns Object containing segments the identity belongs to and any feature overrides */ -function evaluateSegments(context: EvaluationContext): { +export function evaluateSegments(context: EvaluationContext): { segments: EvaluationResult['segments']; - segmentOverrides: Record; + segmentOverrides: Record; } { if (!context.identity || !context.segments) { return { segments: [], segmentOverrides: {} }; @@ -67,8 +69,8 @@ function evaluateSegments(context: EvaluationContext): { * @param identitySegments - Segments that the identity belongs to * @returns Map of feature keys to their highest-priority segment overrides */ -function processSegmentOverrides(identitySegments: any[]): Record { - const segmentOverrides: Record = {}; +export function processSegmentOverrides(identitySegments: any[]): Record { + const segmentOverrides: Record = {}; for (const segment of identitySegments) { if (!segment.overrides) continue; @@ -100,9 +102,9 @@ function processSegmentOverrides(identitySegments: any[]): Record + segmentOverrides: Record ): EvaluationResultFlags { const flags: EvaluationResultFlags = []; @@ -126,9 +128,9 @@ function evaluateFeatures( return flags; } -function shouldApplyOverride( +export function shouldApplyOverride( override: any, - existingOverrides: Record + existingOverrides: Record ): boolean { const currentOverride = existingOverrides[override.feature_key]; return ( @@ -136,11 +138,14 @@ function shouldApplyOverride( ); } -function isHigherPriority(priorityA: number | undefined, priorityB: number | undefined): boolean { +export function isHigherPriority( + priorityA: number | undefined, + priorityB: number | undefined +): boolean { return (priorityA ?? Infinity) < (priorityB ?? Infinity); } -const getTargetingMatchReason = (segmentOverride: segmentOverride) => { +const getTargetingMatchReason = (segmentOverride: SegmentOverride) => { if (segmentOverride) { return segmentOverride.segmentName === IDENTITY_OVERRIDE_SEGMENT_NAME ? TARGETING_REASONS.IDENTITY_OVERRIDE diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 70647c9..18a7d94 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -1,4 +1,11 @@ -import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; +import { + evaluateFeatures, + evaluateSegments, + getEvaluationResult, + isHigherPriority, + SegmentOverrides, + shouldApplyOverride +} from '../../../flagsmith-engine/index.js'; import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js'; import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js'; import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js'; @@ -13,6 +20,9 @@ import { } from './utils.js'; import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; +import { flagsmith } from '../../sdk/utils.js'; +import { getIdentitySegments } from '../../../flagsmith-engine/segments/evaluators.js'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; test('test_get_evaluation_result_without_any_override', () => { const context = getEvaluationContext(environment(), identity()); @@ -146,3 +156,270 @@ test('test_environment_get_all_feature_states', () => { // getEnvironmentFeatureStateFromContext(context, 'not_a_feature_name'); // }).toThrowError('Feature State Not Found'); // }); + +test('isHigherPriority should handle undefined priorities correctly', () => { + expect(isHigherPriority(1, 2)).toBe(true); + expect(isHigherPriority(2, 1)).toBe(false); + expect(isHigherPriority(undefined, 5)).toBe(false); + expect(isHigherPriority(5, undefined)).toBe(true); + expect(isHigherPriority(undefined, undefined)).toBe(false); +}); + +test('shouldApplyOverride with priority conflicts', () => { + const existingOverrides: SegmentOverrides = { + feature1: { + feature: { + key: 'key', + feature_key: 'feature1', + name: 'name', + enabled: true, + value: 'value', + priority: 5 + }, + segmentName: 'segment1' + } + }; + + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 2 }, existingOverrides)).toBe( + true + ); + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 10 }, existingOverrides)).toBe( + false + ); +}); + +test('evaluateSegments handles segments with identity identifier matching', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'segment_with_no_overrides', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [] + }, + '2': { + key: '2', + name: 'segment_with_overrides', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'overridden_value', + priority: 1 + } + ] + } + }, + features: { + feature1: { + key: 'fs1', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'default_value' + } + } + }; + + const result = evaluateSegments(context); + + expect(result.segments).toHaveLength(2); + expect(result.segments).toEqual( + expect.arrayContaining([ + { key: '1', name: 'segment_with_no_overrides' }, + { key: '2', name: 'segment_with_overrides' } + ]) + ); + + expect(Object.keys(result.segmentOverrides)).toEqual(['feature1']); + expect(result.segmentOverrides.feature1.segmentName).toBe('segment_with_overrides'); +}); + +test('evaluateSegments handles priority conflicts correctly', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'low_priority_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'low_priority_value', + priority: 10 + } + ] + }, + '2': { + key: '2', + name: 'high_priority_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override2', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'high_priority_value', + priority: 1 + } + ] + } + }, + features: { + feature1: { + key: 'fs1', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'default_value' + } + } + }; + + const result = evaluateSegments(context); + + expect(result.segments).toHaveLength(2); + + expect(result.segmentOverrides.feature1.segmentName).toBe('high_priority_segment'); + expect(result.segmentOverrides.feature1.feature.value).toBe('high_priority_value'); + expect(result.segmentOverrides.feature1.feature.priority).toBe(1); +}); + +test('evaluateSegments with non-matching identity returns empty', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'segment_for_specific_user', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user-123' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'overridden_value' + } + ] + } + }, + features: {} + }; + + const result = evaluateSegments(context); + + expect(result.segments).toEqual([]); + expect(result.segmentOverrides).toEqual({}); +}); + +test('evaluateFeatures with multivariate evaluation', () => { + const context = { + features: { + mv_feature: { + key: 'mv', + feature_key: 'mv_feature', + name: 'Multivariate Feature', + enabled: true, + value: 'default', + variants: [ + { value: 'variant_a', weight: 0 }, + { value: 'variant_b', weight: 100 } + ] + } + }, + identity: { key: 'test_user', identifier: 'test_user' }, + environment: { + key: 'test_env', + name: 'Test Environment' + } + }; + + const result = evaluateFeatures(context, {}); + expect(result[0].value).toBe('variant_b'); +}); From 829f9582e7a26227627b2f276706851098ebe6bc Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 12:22:59 +0200 Subject: [PATCH 09/25] feat: removed-unused-functions --- flagsmith-engine/evaluationContext/mappers.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts index 0584e1c..307c2c6 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -118,29 +118,6 @@ function mapSegmentRuleModelToRule(rule: any): any { }; } -export function createIdentityContext( - environmentKey: string, - identifier: string, - traits: { [key: string]: any } = {} -): IdentityContext { - return { - identifier, - key: `${environmentKey}_${identifier}`, - traits - }; -} - -export function addIdentityToEvaluationContext( - context: EvaluationContext, - identifier: string, - traits: { [key: string]: any } = {} -): EvaluationContext { - return { - ...context, - identity: createIdentityContext(context.environment.key, identifier, traits) - }; -} - function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { const segments: Segments = {}; From 4bd7cb6ff395b08516711b5ee083eb6043860152 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 15:30:21 +0200 Subject: [PATCH 10/25] feat: improving-evaluator --- flagsmith-engine/segments/evaluators.ts | 75 ++++++++++++++----------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 3a25079..d2bce54 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -19,14 +19,9 @@ export function evaluateIdentityInSegment( segment: SegmentContext, context?: EvaluationContext ): boolean { - const result = - segment.rules.length > 0 && - segment.rules.filter(rule => { - const ruleResult = traitsMatchSegmentRule(rule, segment.key, context); - return ruleResult; - }).length === segment.rules.length; - - return result; + if (segment.rules.length === 0) return false; + + return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); } export function traitsMatchSegmentCondition( @@ -34,7 +29,6 @@ export function traitsMatchSegmentCondition( segmentKey: string, context?: EvaluationContext ): boolean { - const traits = context?.identity?.traits || {}; const identityKey = context?.identity?.key || ''; if (condition.operator === PERCENTAGE_SPLIT) { @@ -44,17 +38,13 @@ export function traitsMatchSegmentCondition( if (!condition.property) { return false; } - let traitValue = traits[condition.property]; - if (condition?.property?.startsWith('$.')) { - traitValue = getContextValue(condition.property, context); - } else { - traitValue = traits[condition.property]; - } + const traitValue = getTraitValue(condition.property, context); if (condition.operator === IS_SET) { return traitValue !== undefined && traitValue !== null; - } else if (condition.operator === IS_NOT_SET) { + } + if (condition.operator === IS_NOT_SET) { return traitValue === undefined || traitValue === null; } @@ -75,26 +65,38 @@ function traitsMatchSegmentRule( segmentKey: string, context?: EvaluationContext ): boolean { - const matchesConditions = - rule.conditions && rule.conditions.length > 0 - ? evaluateRuleConditions( - rule.type, - rule.conditions.map((condition: SegmentCondition) => - traitsMatchSegmentCondition(condition, segmentKey, context) - ) - ) - : true; - - const matchesSubRules = - rule.rules && rule.rules.length > 0 - ? rule.rules.filter((subRule: SegmentRule) => - traitsMatchSegmentRule(subRule, segmentKey, context) - ).length === rule.rules.length - : true; + const matchesConditions = evaluateConditions(rule, segmentKey, context); + const matchesSubRules = evaluateSubRules(rule, segmentKey, context); return matchesConditions && matchesSubRules; } +function evaluateConditions( + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext +): boolean { + if (!rule.conditions || rule.conditions.length === 0) return true; + + const conditionResults = rule.conditions.map((condition: SegmentCondition) => + traitsMatchSegmentCondition(condition, segmentKey, context) + ); + + return evaluateRuleConditions(rule.type, conditionResults); +} + +function evaluateSubRules( + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext +): boolean { + if (!rule.rules || rule.rules.length === 0) return true; + + return rule.rules.every((subRule: SegmentRule) => + traitsMatchSegmentRule(subRule, segmentKey, context) + ); +} + function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): boolean { switch (ruleType) { case 'ALL': @@ -108,6 +110,15 @@ function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): } } +function getTraitValue(property: string, context?: EvaluationContext): any { + if (property.startsWith('$.')) { + return getContextValue(property, context); + } + + const traits = context?.identity?.traits || {}; + return traits[property]; +} + function getContextValue(jsonPath: string, context?: EvaluationContext): any { if (!context) return undefined; From 1b111293de9752cdadfc296e9d93a7b851a44795 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 16:43:41 +0200 Subject: [PATCH 11/25] feat: added-json-path --- flagsmith-engine/segments/evaluators.ts | 19 +- package-lock.json | 181 ++++++++++++ package.json | 2 + .../unit/segments/segment_evaluators.test.ts | 269 +++++++++++++++++- 4 files changed, 450 insertions(+), 21 deletions(-) diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index d2bce54..bbe70a8 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,3 +1,4 @@ +import * as jsonpath from 'jsonpath'; import { EvaluationContext, SegmentCondition, @@ -119,17 +120,13 @@ function getTraitValue(property: string, context?: EvaluationContext): any { return traits[property]; } -function getContextValue(jsonPath: string, context?: EvaluationContext): any { - if (!context) return undefined; +export function getContextValue(jsonPath: string, context?: EvaluationContext): any { + if (!context || !jsonPath.startsWith('$.')) return undefined; - switch (jsonPath) { - case '$.identity.identifier': - return context.identity?.identifier; - case '$.environment.name': - return context.environment?.name; - case '$.environment.key': - return context.environment?.key; - default: - return undefined; + try { + const results = jsonpath.query(context, jsonPath); + return results.length > 0 ? results[0] : undefined; + } catch (error) { + return undefined; } } diff --git a/package-lock.json b/package-lock.json index f4fd0e5..330811c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "6.1.0", "license": "MIT", "dependencies": { + "jsonpath": "^1.1.1", "pino": "^8.8.0", "semver": "^7.3.7", "undici-types": "^6.19.8" }, "devDependencies": { "@types/jest": "^30.0.0", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", @@ -1130,6 +1132,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", @@ -1589,6 +1598,12 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2033,6 +2048,62 @@ "node": ">=8" } }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2043,6 +2114,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2087,6 +2167,12 @@ "node": ">=12.0.0" } }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -2515,6 +2601,30 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2660,6 +2770,23 @@ "node": ">=14.0.0" } }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2798,6 +2925,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -3012,6 +3147,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3049,6 +3194,15 @@ "dev": true, "license": "MIT" }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -3235,6 +3389,18 @@ "node": ">=8.0" } }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -3248,6 +3414,12 @@ "node": ">=4.2.0" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, "node_modules/undici": { "version": "6.21.2", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", @@ -3500,6 +3672,15 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", diff --git a/package.json b/package.json index 3274ac6..2e81194 100644 --- a/package.json +++ b/package.json @@ -63,12 +63,14 @@ "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types" }, "dependencies": { + "jsonpath": "^1.1.1", "pino": "^8.8.0", "semver": "^7.3.7", "undici-types": "^6.19.8" }, "devDependencies": { "@types/jest": "^30.0.0", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 77356b3..a0c4d98 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -3,17 +3,23 @@ import { CONDITION_OPERATORS, PERCENTAGE_SPLIT } from '../../../../flagsmith-engine/segments/constants.js'; -import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js'; + import { traitsMatchSegmentCondition, - evaluateIdentityInSegment + evaluateIdentityInSegment, + getContextValue, + getIdentitySegments } from '../../../../flagsmith-engine/segments/evaluators.js'; import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js'; import { environment } from '../utils.js'; import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js'; import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; import { getEvaluationContext } from '../../../../flagsmith-engine/evaluationContext/mappers.js'; -import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; +import { + EvaluationContext, + SegmentCondition, + SegmentContext +} from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; // todo: work out how to implement this in a test function or before hook vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({ @@ -50,11 +56,11 @@ let traitExistenceTestCases: [ test('test_traits_match_segment_condition_for_trait_existence_operators', () => { for (const testCase of traitExistenceTestCases) { const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase; - let segmentConditionModel = new SegmentConditionModel( + let segmentConditionModel = { operator, - conditionValue, - conditionProperty - ); + value: conditionValue, + property: conditionProperty + }; const traitsMap = traits.reduce((acc, trait) => { acc[trait.traitKey] = trait.traitValue; return acc; @@ -70,9 +76,9 @@ test('test_traits_match_segment_condition_for_trait_existence_operators', () => identifier: 'any' } }; - expect(traitsMatchSegmentCondition(segmentConditionModel, 'any', context)).toBe( - expectedResult - ); + expect( + traitsMatchSegmentCondition(segmentConditionModel as SegmentCondition, 'any', context) + ).toBe(expectedResult); } }); @@ -119,3 +125,246 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen context.identity!.key ]); }); + +describe('getIdentitySegments integration', () => { + test('returns only matching segments', () => { + const context: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'user', + identifier: 'premium@example.com', + traits: { subscription: 'premium' } + }, + segments: { + '1': { + key: '1', + name: 'premium_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'premium' } + ] + } + ], + overrides: [] + }, + '2': { + key: '2', + name: 'basic_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'basic' } + ] + } + ], + overrides: [] + } + }, + features: {} + }; + + const result = getIdentitySegments(context); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('premium_users'); + }); + + test('returns empty array when no segments match', () => { + const context: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'user', + identifier: 'test@example.com', + traits: { subscription: 'free' } + }, + segments: { + '1': { + key: '1', + name: 'premium_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'premium' } + ] + } + ], + overrides: [] + } + }, + features: {} + }; + + const result = getIdentitySegments(context); + expect(result).toEqual([]); + }); +}); + +describe('evaluateIdentityInSegment', () => { + const mockContext: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { key: 'user', identifier: 'test@example.com', traits: { age: 25 } }, + segments: {}, + features: {} + }; + + test('returns false for segment with no rules', () => { + const segment: SegmentContext = { + key: '1', + name: 'empty_segment', + rules: [], + overrides: [] + }; + + expect(evaluateIdentityInSegment(segment, mockContext)).toBe(false); + }); + + test('returns true when all rules match', () => { + const segment: SegmentContext = { + key: '1', + name: 'matching_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ] + }, + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'CONTAINS', + value: 'test@example.com' + } + ] + } + ], + overrides: [] + }; + + expect(evaluateIdentityInSegment(segment, mockContext)).toBe(true); + }); + + test('returns false when any rule fails', () => { + const segment: SegmentContext = { + key: '1', + name: 'failing_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ] + }, + { + type: 'ALL', + conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }] + } + ], + overrides: [] + }; + + expect(evaluateIdentityInSegment(segment, mockContext)).toBe(false); + }); +}); + +describe('getContextValue', () => { + const mockContext: EvaluationContext = { + environment: { + key: 'test-env-key', + name: 'Test Environment' + }, + identity: { + key: 'user-123', + identifier: 'user@example.com' + }, + segments: {}, + features: {} + }; + + // Success cases + test.each([ + ['$.identity.identifier', 'user@example.com'], + ['$.environment.name', 'Test Environment'], + ['$.environment.key', 'test-env-key'] + ])('returns correct value for path %s', (jsonPath, expected) => { + const result = getContextValue(jsonPath, mockContext); + expect(result).toBe(expected); + }); + + // Undefined or invalid cases + test.each([ + ['$.identity.traits.user_type', 'unsupported nested path'], + ['identity.identifier', 'missing $ prefix'], + ['$.invalid.path', 'completely invalid path'], + ['$.identity.nonexistent', 'valid structure but missing property'], + ['', 'empty string'], + ['$', 'just $ symbol'] + ])('returns undefined for %s (%s)', jsonPath => { + const result = getContextValue(jsonPath, mockContext); + expect(result).toBeUndefined(); + }); + + // Context error cases + test.each([ + [undefined, '$.identity.identifier', 'undefined context'], + [{ segments: {}, features: {} }, '$.identity.identifier', 'missing identity'], + [ + { identity: { key: 'test', identifier: 'test' }, segments: {}, features: {} }, + '$.environment.name', + 'missing environment' + ] + ])('returns undefined when %s', (context, jsonPath, _) => { + const result = getContextValue(jsonPath, context as EvaluationContext); + expect(result).toBeUndefined(); + }); +}); + +describe('percentage split operator', () => { + const mockContext: EvaluationContext = { + environment: { key: 'env', name: 'Test Env' }, + identity: { + key: 'user-123', + identifier: 'test@example.com', + traits: { + age: 25, + subscription: 'premium', + active: true + } + }, + segments: {}, + features: {} + }; + beforeEach(() => { + vi.clearAllMocks(); + }); + + test.each([ + [25.5, 30, true], + [25.5, 20, false], + [25.5, 25.5, true], + [0, 0, true], + [100, 99.9, false] + ])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => { + const mockHashFn = getHashedPercentageForObjIds; + mockHashFn.mockReturnValue(hashedValue); + const condition = { property: 'any', operator: 'PERCENTAGE_SPLIT', value: threshold }; + const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext); + + expect(result).toBe(expected); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith(['seg1', 'user-123']); + }); +}); From b2c7499d978e4a13a79f4f7fc950a544bb76e39d Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 16:49:54 +0200 Subject: [PATCH 12/25] feat: added-js-doc-to-evaluators --- flagsmith-engine/segments/evaluators.ts | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index bbe70a8..9cf77a5 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -9,6 +9,15 @@ import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; import { SegmentConditionModel } from './models.js'; import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; +/** + * Returns all segments that the identity belongs to based on segment rules evaluation. + * + * An identity belongs to a segment if it matches ALL of the segment's rules. + * If the context has no identity or segments, returns an empty array. + * + * @param context - Evaluation context containing identity and segment definitions + * @returns Array of segments that the identity matches + */ export function getIdentitySegments(context: EvaluationContext): SegmentContext[] { if (!context.identity || !context.segments) return []; return Object.values(context.segments).filter(segment => @@ -25,6 +34,20 @@ export function evaluateIdentityInSegment( return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); } +/** + * Evaluates whether a segment condition matches the identity's traits or context values. + * + * Handles different types of conditions: + * - PERCENTAGE_SPLIT: Deterministic percentage-based bucketing using identity key + * - IS_SET/IS_NOT_SET: Checks for trait existence + * - Standard operators: EQUAL, NOT_EQUAL, etc. via SegmentConditionModel + * - JSONPath expressions: $.identity.identifier, $.environment.name, etc. + * + * @param condition - The condition to evaluate (property, operator, value) + * @param segmentKey - Key of the segment (used for percentage split hashing) + * @param context - Evaluation context containing identity, traits, and environment + * @returns true if the condition matches + */ export function traitsMatchSegmentCondition( condition: SegmentCondition, segmentKey: string, @@ -120,6 +143,20 @@ function getTraitValue(property: string, context?: EvaluationContext): any { return traits[property]; } +/** + * Evaluates JSONPath expressions against the evaluation context. + * + * Supports accessing nested context values using JSONPath syntax. + * Commonly used paths: + * - $.identity.identifier - User's unique identifier + * - $.identity.key - User's internal key + * - $.environment.name - Environment name + * - $.environment.key - Environment key + * + * @param jsonPath - JSONPath expression starting with '$.' + * @param context - Evaluation context to query against + * @returns The resolved value, or undefined if path doesn't exist or is invalid + */ export function getContextValue(jsonPath: string, context?: EvaluationContext): any { if (!context || !jsonPath.startsWith('$.')) return undefined; From 688e764c2ad391d5550846c6df122a8638f14208 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 19 Sep 2025 17:08:15 +0200 Subject: [PATCH 13/25] feat: abstracted-from-segment-result --- flagsmith-engine/segments/models.ts | 89 ++++++++++--------- .../unit/segments/segments_model.test.ts | 78 ++++++++++++++++ 2 files changed, 127 insertions(+), 40 deletions(-) diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 5dfc09f..db3faff 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -21,6 +21,7 @@ import { isSemver } from './util.js'; import { EvaluationResultSegments } from '../evaluationResult/models.js'; import { EvaluationContext } from '../evaluationContext/evaluationContext.types.js'; import { CONSTANTS } from '../features/constants.js'; +import { SegmentContext } from '../evaluationResult/evaluationResult.types.js'; export const all = (iterable: Array) => iterable.filter(e => !!e).length === iterable.length; export const any = (iterable: Array) => iterable.filter(e => !!e).length > 0; @@ -167,49 +168,57 @@ export class SegmentModel { if (segmentContext) { const segment = new SegmentModel(parseInt(segmentContext.key), segmentContext.name); segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type)); - segment.featureStates = - segmentContext.overrides?.map(override => { - const feature = new FeatureModel( - parseInt(override.feature_key), - override.name, - override?.variants?.length && override?.variants?.length > 1 - ? CONSTANTS.MULTIVARIATE - : CONSTANTS.STANDARD - ); - - const featureState = new FeatureStateModel( - feature, - override.enabled, - override.priority || 0 - ); - - if (override.value !== undefined) { - featureState.setValue(override.value); - } - - if ( - override.variants && - override?.variants?.length > 1 && - override.variants.length > 0 - ) { - featureState.multivariateFeatureStateValues = override.variants.map( - variant => - new MultivariateFeatureStateValueModel( - new MultivariateFeatureOptionModel( - variant.value, - variant?.id as number - ), - variant.weight as number, - variant.id as number - ) - ); - } - - return featureState; - }) || []; + segment.featureStates = SegmentModel.createFeatureStatesFromOverrides( + segmentContext.overrides || [] + ); segmentModels.push(segment); } } + return segmentModels; } + + private static createFeatureStatesFromOverrides( + overrides: SegmentContext['overrides'] + ): FeatureStateModel[] { + if (!overrides) return []; + return overrides.map(override => { + const feature = new FeatureModel( + parseInt(override.feature_key), + override.name, + override.variants?.length && override.variants.length > 0 + ? CONSTANTS.MULTIVARIATE + : CONSTANTS.STANDARD + ); + + const featureState = new FeatureStateModel( + feature, + override.enabled, + override.priority || 0 + ); + + if (override.value !== undefined) { + featureState.setValue(override.value); + } + + if (override.variants && override.variants.length > 0) { + featureState.multivariateFeatureStateValues = this.createMultivariateValues( + override.variants + ); + } + + return featureState; + }); + } + + private static createMultivariateValues(variants: any[]): MultivariateFeatureStateValueModel[] { + return variants.map( + variant => + new MultivariateFeatureStateValueModel( + new MultivariateFeatureOptionModel(variant.value, variant.id as number), + variant.weight as number, + variant.id as number + ) + ); + } } diff --git a/tests/engine/unit/segments/segments_model.test.ts b/tests/engine/unit/segments/segments_model.test.ts index 17d9166..5607f03 100644 --- a/tests/engine/unit/segments/segments_model.test.ts +++ b/tests/engine/unit/segments/segments_model.test.ts @@ -1,3 +1,5 @@ +import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types'; +import { CONSTANTS } from '../../../../flagsmith-engine/features/constants'; import { ALL_RULE, ANY_RULE, @@ -8,6 +10,7 @@ import { all, any, SegmentConditionModel, + SegmentModel, SegmentRuleModel } from '../../../../flagsmith-engine/segments/models'; @@ -135,3 +138,78 @@ test('test_segment_rule_matching_function', () => { expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]); } }); + +test('test_fromSegmentResult_with_multiple_variants', () => { + const segmentResults = [{ key: '1', name: 'test_segment' }]; + + const evaluationContext: EvaluationContext = { + identity: { + key: 'not_exist', + identifier: 'not_exist' + }, + environment: { + key: 'test', + name: 'test' + }, + features: {}, + segments: { + '1': { + key: '1', + name: 'test_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override', + feature_key: '1', + name: 'multivariate_feature', + enabled: true, + value: 'default_value', + priority: 1, + variants: [ + { id: 1, value: 'variant_a', weight: 30 }, + { id: 2, value: 'variant_b', weight: 70 } + ] + } + ] + } + } + }; + + const result = SegmentModel.fromSegmentResult(segmentResults, evaluationContext); + + expect(result).toHaveLength(1); + + const segment = result[0]; + expect(segment.name).toBe('test_segment'); + expect(segment.featureStates).toHaveLength(1); + + const featureState = segment.featureStates[0]; + expect(featureState.feature.name).toBe('multivariate_feature'); + expect(featureState.feature.type).toBe(CONSTANTS.MULTIVARIATE); + expect(featureState.enabled).toBe(true); + expect(featureState.getValue()).toBe('default_value'); + + // Test multivariate variants + expect(featureState.multivariateFeatureStateValues).toHaveLength(2); + + const variant1 = featureState.multivariateFeatureStateValues[0]; + expect(variant1.multivariateFeatureOption.value).toBe('variant_a'); + expect(variant1.percentageAllocation).toBe(30); + expect(variant1.id).toBe(1); + + const variant2 = featureState.multivariateFeatureStateValues[1]; + expect(variant2.multivariateFeatureOption.value).toBe('variant_b'); + expect(variant2.percentageAllocation).toBe(70); + expect(variant2.id).toBe(2); +}); From 88cddd33f63375e2bce2531898fadf45c2e2cb81 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 22 Sep 2025 11:12:17 +0200 Subject: [PATCH 14/25] chore: updated-tests-to-new-data --- tests/engine/e2e/engine.test.ts | 6 +++--- tests/engine/engine-tests/engine-test-data | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index dd22072..f565745 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -10,7 +10,7 @@ function extractTestCases(data: any): { const test_data = data['test_cases'].map((test_case: any) => { return { context: test_case['context'], - response: test_case['response'] + response: test_case['result'] }; }); return test_data; @@ -25,13 +25,13 @@ test('Test Engine', () => { .allFlags() .sort((a, b) => (a.featureName > b.featureName ? 1 : -1)); const sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) => - a.feature.name > b.feature.name ? 1 : -1 + a.name > b.name ? 1 : -1 ); expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); for (let i = 0; i < sortedEngineFlags.length; i++) { - expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i]['feature_state_value']); + expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i].value); expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i]['enabled']); } } diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 5e7c413..18c68ef 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 5e7c4139c59e529301f7dc8f784e991f1c8840fb +Subproject commit 18c68ef925910622a228af2892aed48b21e532fe From dde91bd76e2d9d56b3539056461b968580033a8d Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 23 Sep 2025 14:47:51 +0200 Subject: [PATCH 15/25] feat: removed-hide-disabled-in-local-evaluation --- flagsmith-engine/evaluationContext/mappers.ts | 1 + flagsmith-engine/index.ts | 3 -- flagsmith-engine/segments/evaluators.ts | 2 + flagsmith-engine/segments/models.ts | 4 ++ tests/engine/unit/engine.test.ts | 52 ------------------- 5 files changed, 7 insertions(+), 55 deletions(-) diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts index 307c2c6..604125b 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -134,6 +134,7 @@ function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { priority: -Infinity })); + // Can be grouped in a massive IN segment with all the overrides const segmentKey = `identity_override_${identity.identifier}`; segments[segmentKey] = { diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index fbb278f..dd80bcf 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -30,9 +30,6 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul const { segments, segmentOverrides } = evaluateSegments(context); const flags = evaluateFeatures(context, segmentOverrides); - // Not sure if we need this - Keeping till confirmed hidedisabledflags is remote evaluation only - // const filteredFlags = hideDisabledFlags ? flags.filter(flag => flag.enabled) : flags; - return { context, flags, segments }; } diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 9cf77a5..0bf37ae 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -53,6 +53,8 @@ export function traitsMatchSegmentCondition( segmentKey: string, context?: EvaluationContext ): boolean { + // This could be any context value and identity key is the fallback ($.environment.key / $.environment.name ...) => getContextValue + // We need to re-implement the IN operator for context values (especially because of the JSONEncodedList + context values) const identityKey = context?.identity?.key || ''; if (condition.operator === PERCENTAGE_SPLIT) { diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index db3faff..3180e98 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -99,6 +99,10 @@ export class SegmentConditionModel { return traitValue % divisor === reminder; }, evaluateIn: (traitValue: any) => { + // Looks for a list => all good but checks if it's a list of string + // If it's a string => Assume it's a json encoded list + // Fallback to the old logic + // Add some tests return this.value?.split(',').includes(traitValue.toString()); } }; diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 18a7d94..6f82c35 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -20,8 +20,6 @@ import { } from './utils.js'; import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; -import { flagsmith } from '../../sdk/utils.js'; -import { getIdentitySegments } from '../../../flagsmith-engine/segments/evaluators.js'; import { EvaluationContext } from '../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; test('test_get_evaluation_result_without_any_override', () => { @@ -89,18 +87,6 @@ test('test_identity_get_all_feature_states_with_traits', () => { ); }); -// TO CONFIRM ITS REMOVED -// test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => { -// const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); - -// const env = environmentWithSegmentOverride(); - -// const context = getEvaluationContext(env, identityInSegment(), [trait_models]); -// const result = getEvaluationResult(context, true); - -// expect(result.flags.length).toBe(0); -// }); - test('test_environment_get_all_feature_states', () => { const env = environment(); const context = getEvaluationContext(env); @@ -118,44 +104,6 @@ test('test_environment_get_all_feature_states', () => { expect(flag.value).toBe(envFeature?.value); } }); -// CONFIRM hide_disabled_flags is removed in local evaluation -// test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => { -// // One feature is disabled this environment -// const env = environment(); -// const context = getEvaluationContext(env); -// const result = getEvaluationResult(context, true); - -// expect(result.flags.length).toBe(1); - -// result.flags.forEach(flag => { -// expect(flag.reason).toBe('DEFAULT'); -// }); - -// for (const flag of result.flags) { -// const envFeature = Object.values(context.features || {}).find(f => f.name === flag.name); -// expect(flag.enabled).toBe(envFeature?.enabled); -// expect(flag.value).toBe(envFeature?.value); -// } -// }); - -// Check if this test is still needed -// test('test_environment_get_feature_state', () => { -// const env = environment(); -// const feature = feature1(); -// const context = getEvaluationContext(env, identity()); -// const featureState = getEnvironmentFeatureStateFromContext(context, feature.name); - -// expect(featureState.name).toStrictEqual(feature.name); -// }); - -// Check if this test is still needed -// test('test_environment_get_feature_state_raises_feature_state_not_found', () => { -// const context = getEvaluationContext(environment(), identity()); -// const result = getEvaluationResult(context); -// expect(() => { -// getEnvironmentFeatureStateFromContext(context, 'not_a_feature_name'); -// }).toThrowError('Feature State Not Found'); -// }); test('isHigherPriority should handle undefined priorities correctly', () => { expect(isHigherPriority(1, 2)).toBe(true); From 88777e60870c52040b4029444ed72fdbc1ab7276 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 23 Sep 2025 15:32:01 +0200 Subject: [PATCH 16/25] feat: process-identity-overrides-as-in-segment --- flagsmith-engine/evaluationContext/mappers.ts | 101 +++++++++++------- tests/engine/unit/engine.test.ts | 9 +- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluationContext/mappers.ts index 604125b..0f875b8 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluationContext/mappers.ts @@ -10,6 +10,7 @@ import { EnvironmentModel } from '../environments/models.js'; import { IdentityModel } from '../identities/models.js'; import { TraitModel } from '../identities/traits/models.js'; import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../segments/constants.js'; +import { createHash } from 'node:crypto'; export function getEvaluationContext( environment: EnvironmentModel, @@ -23,11 +24,7 @@ export function getEvaluationContext( const context = { ...environmentContext, - ...(identityContext && { identity: identityContext }), - segments: { - ...environmentContext.segments, - ...(identity && mapIdentityOverridesToSegments(identity)) - } + ...(identityContext && { identity: identityContext }) }; return context; @@ -61,9 +58,9 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): }; } - const segments: Segments = {}; + const segmentOverrides: Segments = {}; for (const segment of environment.project.segments) { - segments[segment.id.toString()] = { + segmentOverrides[segment.id.toString()] = { key: segment.id.toString(), name: segment.name, rules: segment.rules.map(rule => mapSegmentRuleModelToRule(rule)), @@ -81,10 +78,18 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): }; } + let identityOverrideSegments: Segments = {}; + if (environment.identityOverrides && environment.identityOverrides.length > 0) { + identityOverrideSegments = mapIdentityOverridesToSegments(environment.identityOverrides); + } + return { environment: environmentContext, features, - segments + segments: { + ...segmentOverrides, + ...identityOverrideSegments + } }; } @@ -118,42 +123,56 @@ function mapSegmentRuleModelToRule(rule: any): any { }; } -function mapIdentityOverridesToSegments(identity: IdentityModel): Segments { +function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Segments { const segments: Segments = {}; + const featuresToIdentifiers = new Map(); - if (!identity.identityFeatures || identity.identityFeatures.length === 0) { - return segments; - } + for (const identity of identityOverrides) { + if (!identity.identityFeatures || identity.identityFeatures.length === 0) { + continue; + } - const overrides = identity.identityFeatures.map(fs => ({ - key: fs.djangoID?.toString() || fs.featurestateUUID, - feature_key: fs.feature.id.toString(), - name: fs.feature.name, - enabled: fs.enabled, - value: fs.getValue(), - priority: -Infinity - })); - - // Can be grouped in a massive IN segment with all the overrides - const segmentKey = `identity_override_${identity.identifier}`; - - segments[segmentKey] = { - key: segmentKey, - name: IDENTITY_OVERRIDE_SEGMENT_NAME, - rules: [ - { - type: 'ALL', - conditions: [ - { - property: '$.identity.identifier', - operator: 'EQUAL', - value: identity.identifier - } - ] - } - ], - overrides - }; + const sortedFeatures = [...identity.identityFeatures].sort((a, b) => + a.feature.name.localeCompare(b.feature.name) + ); + const overridesKey = sortedFeatures.map(fs => ({ + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + priority: -Infinity + })); + + const overridesHash = createHash('sha1').update(JSON.stringify(overridesKey)).digest('hex'); + + if (!featuresToIdentifiers.has(overridesHash)) { + featuresToIdentifiers.set(overridesHash, { identifiers: [], overrides: overridesKey }); + } + + featuresToIdentifiers.get(overridesHash)!.identifiers.push(identity.identifier); + } + for (const [overrideHash, { identifiers, overrides }] of featuresToIdentifiers.entries()) { + const segmentKey = `identity_override_${overrideHash}`; + + segments[segmentKey] = { + key: segmentKey, + name: IDENTITY_OVERRIDE_SEGMENT_NAME, + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'IN', + // TODO: Modify once new IN operator is implemented + value: identifiers.join(',') + } + ] + } + ], + overrides: overrides + }; + } return segments; } diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 6f82c35..77d497a 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -33,14 +33,6 @@ test('test_get_evaluation_result_without_any_override', () => { expect(flag?.reason).toBe(TARGETING_REASONS.DEFAULT); }); -// CHECK IF THIS TEST IS STILL NEEDED -// test('test_identity_get_feature_state_from_contextwithout_any_override_no_fs', () => { -// expect(() => { -// const context = getEvaluationContext(environment(), identity()); -// getIdentityFeatureStateFromContext(context, 'nonExistentName'); -// }).toThrowError('Feature State Not Found'); -// }); - test('test_get_evaluation_result_with_identity_override_and_no_segment_override', () => { const env = environment(); const ident = identity(); @@ -48,6 +40,7 @@ test('test_get_evaluation_result_with_identity_override_and_no_segment_override' env.featureStates.push(new FeatureStateModel(overridden_feature, false, 3)); ident.identityFeatures = [new FeatureStateModel(overridden_feature, true, 4)]; + env.identityOverrides = [ident]; const context = getEvaluationContext(env, ident); const result = getEvaluationResult(context); From ac8b5464af5e46243a55731000e6d4081742267f Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 23 Sep 2025 17:31:45 +0200 Subject: [PATCH 17/25] feat: re-implemented-in-condition --- flagsmith-engine/evaluationContext/models.ts | 2 +- flagsmith-engine/segments/evaluators.ts | 13 ++- flagsmith-engine/segments/models.ts | 29 +++-- .../unit/segments/segment_evaluators.test.ts | 101 +++++++++++++++++- 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/flagsmith-engine/evaluationContext/models.ts b/flagsmith-engine/evaluationContext/models.ts index 8b3cd9f..8f7a561 100644 --- a/flagsmith-engine/evaluationContext/models.ts +++ b/flagsmith-engine/evaluationContext/models.ts @@ -22,7 +22,7 @@ export type SegmentKey = SegmentContext['key']; export type SegmentName = SegmentContext['name']; export type SegmentRuleType = SegmentRule['type']; export type ConditionOperator = SegmentCondition['operator'] | InSegmentCondition['operator']; -export type ConditionProperty = SegmentCondition['property']; +export type ConditionProperty = SegmentCondition['property'] | InSegmentCondition['property']; export type ConditionValue = SegmentCondition['value'] | InSegmentCondition['value']; export type FeatureKey = FeatureContext['feature_key']; diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 0bf37ae..b2c6e3e 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,6 +1,7 @@ import * as jsonpath from 'jsonpath'; import { EvaluationContext, + InSegmentCondition, SegmentCondition, SegmentContext, SegmentRule @@ -49,16 +50,14 @@ export function evaluateIdentityInSegment( * @returns true if the condition matches */ export function traitsMatchSegmentCondition( - condition: SegmentCondition, + condition: SegmentCondition | InSegmentCondition, segmentKey: string, context?: EvaluationContext ): boolean { - // This could be any context value and identity key is the fallback ($.environment.key / $.environment.name ...) => getContextValue - // We need to re-implement the IN operator for context values (especially because of the JSONEncodedList + context values) - const identityKey = context?.identity?.key || ''; - if (condition.operator === PERCENTAGE_SPLIT) { - const hashedPercentage = getHashedPercentageForObjIds([segmentKey, identityKey]); + const contextValueKey = + getContextValue(condition.property, context) || context?.identity?.key; + const hashedPercentage = getHashedPercentageForObjIds([segmentKey, contextValueKey]); return hashedPercentage <= parseFloat(String(condition.value)); } if (!condition.property) { @@ -160,7 +159,7 @@ function getTraitValue(property: string, context?: EvaluationContext): any { * @returns The resolved value, or undefined if path doesn't exist or is invalid */ export function getContextValue(jsonPath: string, context?: EvaluationContext): any { - if (!context || !jsonPath.startsWith('$.')) return undefined; + if (!context || !jsonPath?.startsWith('$.')) return undefined; try { const results = jsonpath.query(context, jsonPath); diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 3180e98..69d504c 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -65,12 +65,12 @@ export class SegmentConditionModel { }; operator: string; - value: string | null | undefined; + value: string | null | undefined | string[]; property: string | null | undefined; constructor( operator: string, - value?: string | null | undefined, + value?: string | null | undefined | string[], property?: string | null | undefined ) { this.operator = operator; @@ -88,21 +88,32 @@ export class SegmentConditionModel { ); }, evaluateRegex: (traitValue: any) => { - return !!this.value && !!traitValue?.toString().match(new RegExp(this.value)); + return ( + !!this.value && + !!traitValue?.toString().match(new RegExp(this.value?.toString())) + ); }, evaluateModulo: (traitValue: any) => { if (isNaN(parseFloat(traitValue)) || !this.value) { return false; } - const parts = this.value.split('|'); + const parts = this.value?.toString().split('|'); const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])]; return traitValue % divisor === reminder; }, - evaluateIn: (traitValue: any) => { - // Looks for a list => all good but checks if it's a list of string - // If it's a string => Assume it's a json encoded list - // Fallback to the old logic - // Add some tests + evaluateIn: (traitValue: string[] | string) => { + if (Array.isArray(this.value)) { + return this.value.includes(traitValue.toString()); + } + + if (typeof this.value === 'string') { + try { + const parsed = JSON.parse(this.value); + if (Array.isArray(parsed)) { + return parsed.includes(traitValue.toString()); + } + } catch {} + } return this.value?.split(',').includes(traitValue.toString()); } }; diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index a0c4d98..61f02e3 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -17,9 +17,12 @@ import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils import { getEvaluationContext } from '../../../../flagsmith-engine/evaluationContext/mappers.js'; import { EvaluationContext, + InSegmentCondition, SegmentCondition, + SegmentCondition1, SegmentContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; +import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js'; // todo: work out how to implement this in a test function or before hook vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({ @@ -203,6 +206,98 @@ describe('getIdentitySegments integration', () => { }); }); +describe('IN operator', () => { + const mockContext: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'test-user', + identifier: 'test', + traits: { name: 'test' } + }, + segments: {}, + features: {} + }; + + test.each([ + // Array of strings + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: ['test', 'john-doe'] + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: ['john-doe'] + }, + false + ], + + // JSON encoded + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: '["test", "john-doe"]' + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: '["john-doe"]' + }, + false + ], + + // Legacy value string to split + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'test,john-doe' + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'john-doe' + }, + false + ], + // Fails because the value is split in middle + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'te,st,john-doe' + }, + false + ], + + // Edge cases + [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '' }, false], + [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: [] }, false], + [ + { property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '[]' }, + false + ] + ] as Array<[SegmentCondition | InSegmentCondition, boolean]>)( + 'evaluates IN condition %j to %s', + (condition: SegmentCondition | InSegmentCondition, expected: boolean) => { + const result = traitsMatchSegmentCondition(condition, 'segment', mockContext); + expect(result).toBe(expected); + } + ); +}); + describe('evaluateIdentityInSegment', () => { const mockContext: EvaluationContext = { environment: { key: 'env', name: 'test' }, @@ -361,7 +456,11 @@ describe('percentage split operator', () => { ])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => { const mockHashFn = getHashedPercentageForObjIds; mockHashFn.mockReturnValue(hashedValue); - const condition = { property: 'any', operator: 'PERCENTAGE_SPLIT', value: threshold }; + const condition = { + property: 'any', + operator: 'PERCENTAGE_SPLIT', + value: threshold.toString() + } as SegmentCondition1 | InSegmentCondition; const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext); expect(result).toBe(expected); From 2d998ab0cb58ea9d170d410ac733d79e44efb284 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 23 Sep 2025 21:26:32 +0200 Subject: [PATCH 18/25] feat: pulled-latest-version-of-tests --- tests/engine/engine-tests/engine-test-data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 18c68ef..e07cd18 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 18c68ef925910622a228af2892aed48b21e532fe +Subproject commit e07cd18b38aef93f11bd9c47e018ea01204cca25 From 9a89bf771720ae49989421f9cb8d6dc98979d338 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 24 Sep 2025 12:03:23 +0200 Subject: [PATCH 19/25] feat: removed-targeting-reason-identity --- flagsmith-engine/features/types.ts | 1 - flagsmith-engine/index.ts | 9 +++------ tests/engine/unit/engine.test.ts | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/flagsmith-engine/features/types.ts b/flagsmith-engine/features/types.ts index 8417788..8f7fad1 100644 --- a/flagsmith-engine/features/types.ts +++ b/flagsmith-engine/features/types.ts @@ -1,5 +1,4 @@ export enum TARGETING_REASONS { DEFAULT = 'DEFAULT', - IDENTITY_OVERRIDE = 'IDENTITY_OVERRIDE', TARGETING_MATCH = 'TARGETING_MATCH' } diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index dd80bcf..a47aa91 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -143,10 +143,7 @@ export function isHigherPriority( } const getTargetingMatchReason = (segmentOverride: SegmentOverride) => { - if (segmentOverride) { - return segmentOverride.segmentName === IDENTITY_OVERRIDE_SEGMENT_NAME - ? TARGETING_REASONS.IDENTITY_OVERRIDE - : `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}`; - } - return TARGETING_REASONS.DEFAULT; + return segmentOverride + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}` + : TARGETING_REASONS.DEFAULT; }; diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 77d497a..a70bda3 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -21,6 +21,7 @@ import { import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; import { EvaluationContext } from '../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js'; test('test_get_evaluation_result_without_any_override', () => { const context = getEvaluationContext(environment(), identity()); @@ -57,7 +58,7 @@ test('test_get_evaluation_result_with_identity_override_and_no_segment_override' expect(flag.enabled).toBe(expected); expect(flag.reason).toBe( flag.name === 'overridden_feature' - ? TARGETING_REASONS.IDENTITY_OVERRIDE + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${IDENTITY_OVERRIDE_SEGMENT_NAME}` : TARGETING_REASONS.DEFAULT ); } From 40355a117dda64b15a4c58160313ca407a8878f9 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 24 Sep 2025 12:18:08 +0200 Subject: [PATCH 20/25] feat: merged-evaluation-context-types --- .../evaluationContext.types.ts | 233 ++++++++++++++ .../evaluationContext/mappers.ts | 10 +- .../evaluationContext/types.ts | 0 .../evaluationResult.types.ts | 290 ++++++++++++++++++ .../models.ts | 36 ++- flagsmith-engine/evaluationResult/models.ts | 43 --- flagsmith-engine/features/util.ts | 2 +- flagsmith-engine/index.ts | 5 +- flagsmith-engine/segments/evaluators.ts | 2 +- flagsmith-engine/segments/models.ts | 12 +- sdk/index.ts | 2 +- sdk/models.ts | 5 +- tests/engine/unit/engine.test.ts | 4 +- .../unit/segments/segment_evaluators.test.ts | 5 +- 14 files changed, 577 insertions(+), 72 deletions(-) create mode 100644 flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts rename flagsmith-engine/{ => evaluation}/evaluationContext/mappers.ts (95%) rename flagsmith-engine/{ => evaluation}/evaluationContext/types.ts (100%) create mode 100644 flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts rename flagsmith-engine/{evaluationContext => evaluation}/models.ts (55%) delete mode 100644 flagsmith-engine/evaluationResult/models.ts diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts new file mode 100644 index 0000000..aef9efa --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -0,0 +1,233 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +export type SegmentCondition = SegmentCondition1 | InSegmentCondition; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = SegmentCondition[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; + +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition1 { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} diff --git a/flagsmith-engine/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts similarity index 95% rename from flagsmith-engine/evaluationContext/mappers.ts rename to flagsmith-engine/evaluation/evaluationContext/mappers.ts index 0f875b8..a77b4de 100644 --- a/flagsmith-engine/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -5,11 +5,11 @@ import { EvaluationContext, EnvironmentContext, IdentityContext -} from './models.js'; -import { EnvironmentModel } from '../environments/models.js'; -import { IdentityModel } from '../identities/models.js'; -import { TraitModel } from '../identities/traits/models.js'; -import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../segments/constants.js'; +} from '../models.js'; +import { EnvironmentModel } from '../../environments/models.js'; +import { IdentityModel } from '../../identities/models.js'; +import { TraitModel } from '../../identities/traits/models.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../segments/constants.js'; import { createHash } from 'node:crypto'; export function getEvaluationContext( diff --git a/flagsmith-engine/evaluationContext/types.ts b/flagsmith-engine/evaluation/evaluationContext/types.ts similarity index 100% rename from flagsmith-engine/evaluationContext/types.ts rename to flagsmith-engine/evaluation/evaluationContext/types.ts diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts new file mode 100644 index 0000000..fc0df3b --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -0,0 +1,290 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = (SegmentCondition | InSegmentCondition)[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; +/** + * Unique feature identifier. + */ +export type FeatureKey1 = string; +/** + * Feature name. + */ +export type Name3 = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled1 = boolean; +/** + * Feature flag value. + */ +export type Value4 = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * List of feature flags evaluated for the context. + */ +export type Flags = FlagResult[]; +/** + * Unique segment identifier. + */ +export type Key4 = string; +/** + * Segment name. + */ +export type Name4 = string; +/** + * List of segments which the provided context belongs to. + */ +export type Segments1 = SegmentResult[]; + +/** + * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. + */ +export interface EvaluationResult { + context: EvaluationContext; + flags: Flags; + segments: Segments1; + [k: string]: unknown; +} +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} +export interface FlagResult { + feature_key: FeatureKey1; + name: Name3; + enabled: Enabled1; + value?: Value4; + reason?: Reason; + [k: string]: unknown; +} +export interface SegmentResult { + key: Key4; + name: Name4; + [k: string]: unknown; +} diff --git a/flagsmith-engine/evaluationContext/models.ts b/flagsmith-engine/evaluation/models.ts similarity index 55% rename from flagsmith-engine/evaluationContext/models.ts rename to flagsmith-engine/evaluation/models.ts index 8f7a561..ba9c511 100644 --- a/flagsmith-engine/evaluationContext/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -1,3 +1,7 @@ +// This file is the entry point for the evaluation module types +// All types from evaluations should be at least imported here and re-exported +// Do not use types directly from generated files + import type { EnvironmentContext, IdentityContext, @@ -9,8 +13,14 @@ import type { FeatureValue as ContextFeatureValue, Traits, Features, - Segments -} from './evaluationContext.types.ts'; + Segments, + EvaluationContext +} from './evaluationContext/evaluationContext.types.js'; + +import type { + EvaluationResult as EvaluationContextResult, + FlagResult as EvaluationContextResultFlagResult +} from './evaluationResult/evaluationResult.types.js'; export type EnvironmentKey = EnvironmentContext['key']; export type EnvironmentName = EnvironmentContext['name']; @@ -39,4 +49,24 @@ export type TraitMap = Traits; export type FeatureMap = Features; export type SegmentMap = Segments; -export type * from './evaluationContext.types.ts'; +export type SegmentConditionOperator = SegmentCondition['operator']; + +export type EvaluationReason = EvaluationContextResultFlagResult['reason']; + +export type EvaluationResultSegments = EvaluationContextResult['segments']; +export type EvaluationResultFlags = { + feature_key: FeatureKey; + name: FeatureName; + enabled: FeatureEnabled; + value: FeatureValue; + reason: EvaluationReason; +}[]; + +export type EvaluationResult = { + context: EvaluationContext; + flags: EvaluationResultFlags; + segments: EvaluationResultSegments; +}; + +export type { FlagResult } from './evaluationResult/evaluationResult.types.js'; +export type * from './evaluationContext/evaluationContext.types.js'; diff --git a/flagsmith-engine/evaluationResult/models.ts b/flagsmith-engine/evaluationResult/models.ts deleted file mode 100644 index 0442a1e..0000000 --- a/flagsmith-engine/evaluationResult/models.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { EvaluationContext } from '../evaluationContext/models.ts'; - -import type { - EvaluationResult as EvaluationContextResult, - FlagResult as EvaluationContextResultFlagResult, - SegmentResult, - SegmentCondition, - IdentityContext, - SegmentContext, - EnvironmentContext -} from './evaluationResult.types.ts'; - -export type EnvironmentKey = EnvironmentContext['key']; -export type EnvironmentName = EnvironmentContext['name']; - -export type IdentityIdentifier = IdentityContext['identifier']; -export type IdentityKey = IdentityContext['key']; - -export type SegmentKey = SegmentResult['key']; -export type SegmentName = SegmentResult['name']; -export type SegmentConditionOperator = SegmentCondition['operator']; -export type SegmentRuleType = SegmentContext['rules'][0]['type']; - -export type FeatureKey = EvaluationContextResultFlagResult['feature_key']; -export type FeatureName = EvaluationContextResultFlagResult['name']; -export type FeatureEnabled = EvaluationContextResultFlagResult['enabled']; -export type FeatureValue = EvaluationContextResultFlagResult['value']; -export type EvaluationReason = EvaluationContextResultFlagResult['reason']; - -export type EvaluationResultSegments = EvaluationContextResult['segments']; -export type EvaluationResultFlags = { - feature_key: FeatureKey; - name: FeatureName; - enabled: FeatureEnabled; - value: FeatureValue; - reason: EvaluationReason; -}[]; - -export type EvaluationResult = { - context: EvaluationContext; - flags: EvaluationResultFlags; - segments: EvaluationResultSegments; -}; diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index 202f614..a02cd78 100644 --- a/flagsmith-engine/features/util.ts +++ b/flagsmith-engine/features/util.ts @@ -6,7 +6,7 @@ import { MultivariateFeatureStateValueModel } from './models.js'; -import { FeatureContext } from '../evaluationContext/models.js'; +import { FeatureContext } from '../evaluation/models.js'; import { getHashedPercentageForObjIds as getHashedPercentageForObjIds } from '../utils/hashing/index.js'; export function buildFeatureModel(featuresModelJSON: any): FeatureModel { diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index a47aa91..93dfc73 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,8 +1,7 @@ -import { EvaluationContext, FeatureContext } from './evaluationContext/models.js'; +import { EvaluationContext, FeatureContext } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { EvaluationResult, EvaluationResultFlags } from './evaluationResult/models.js'; +import { EvaluationResult, EvaluationResultFlags } from './evaluation/models.js'; import { evaluateFeatureValue } from './features/util.js'; -import { IDENTITY_OVERRIDE_SEGMENT_NAME } from './segments/constants.js'; import { TARGETING_REASONS } from './features/types.js'; export { EnvironmentModel } from './environments/models.js'; export { IdentityModel } from './identities/models.js'; diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index b2c6e3e..089b04f 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -5,7 +5,7 @@ import { SegmentCondition, SegmentContext, SegmentRule -} from '../evaluationContext/models.js'; +} from '../evaluation/models.js'; import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; import { SegmentConditionModel } from './models.js'; import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 69d504c..98cb98f 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -18,10 +18,12 @@ import { CONDITION_OPERATORS } from './constants.js'; import { isSemver } from './util.js'; -import { EvaluationResultSegments } from '../evaluationResult/models.js'; -import { EvaluationContext } from '../evaluationContext/evaluationContext.types.js'; +import { + EvaluationContext, + Overrides +} from '../evaluation/evaluationContext/evaluationContext.types.js'; import { CONSTANTS } from '../features/constants.js'; -import { SegmentContext } from '../evaluationResult/evaluationResult.types.js'; +import { EvaluationResultSegments } from '../evaluation/models.js'; export const all = (iterable: Array) => iterable.filter(e => !!e).length === iterable.length; export const any = (iterable: Array) => iterable.filter(e => !!e).length > 0; @@ -193,9 +195,7 @@ export class SegmentModel { return segmentModels; } - private static createFeatureStatesFromOverrides( - overrides: SegmentContext['overrides'] - ): FeatureStateModel[] { + private static createFeatureStatesFromOverrides(overrides: Overrides): FeatureStateModel[] { if (!overrides) return []; return overrides.map(override => { const feature = new FeatureModel( diff --git a/sdk/index.ts b/sdk/index.ts index 5d58ac5..7682f2b 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -24,7 +24,7 @@ import { TraitConfig } from './types.js'; import { pino, Logger } from 'pino'; -import { getEvaluationContext } from '../flagsmith-engine/evaluationContext/mappers.js'; +import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js'; export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js'; export { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; diff --git a/sdk/models.ts b/sdk/models.ts index d9896cf..2fdfbed 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,4 @@ -import { - EvaluationResult, - FlagResult -} from '../flagsmith-engine/evaluationResult/evaluationResult.types.js'; +import { EvaluationResult, FlagResult } from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index a70bda3..1d6df05 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -18,9 +18,9 @@ import { segmentConditionProperty, segmentConditionStringValue } from './utils.js'; -import { getEvaluationContext } from '../../../flagsmith-engine/evaluationContext/mappers.js'; +import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; -import { EvaluationContext } from '../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js'; test('test_get_evaluation_result_without_any_override', () => { diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 61f02e3..6470570 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -14,15 +14,14 @@ import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js import { environment } from '../utils.js'; import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js'; import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; -import { getEvaluationContext } from '../../../../flagsmith-engine/evaluationContext/mappers.js'; +import { getEvaluationContext } from '../../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; import { EvaluationContext, InSegmentCondition, SegmentCondition, SegmentCondition1, SegmentContext -} from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js'; -import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js'; +} from '../../../../flagsmith-engine/evaluation/models.js'; // todo: work out how to implement this in a test function or before hook vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({ From a4fe22306f04c11391d2399e7ad11cbac4b2cb52 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 29 Sep 2025 10:22:27 +0200 Subject: [PATCH 21/25] feat: removed-unnecessary-abstraction --- .../evaluationContext.types.ts | 2 +- .../evaluationResult.types.ts | 2 +- flagsmith-engine/segments/evaluators.ts | 15 +- .../unit/segments/segment_evaluators.test.ts | 135 ++++++++++-------- 4 files changed, 83 insertions(+), 71 deletions(-) diff --git a/flagsmith-engine/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluationContext/evaluationContext.types.ts index aef9efa..d105c96 100644 --- a/flagsmith-engine/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluationContext/evaluationContext.types.ts @@ -113,7 +113,7 @@ export type Value3 = string | number | boolean | null; */ export type Weight = number; /** - * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. */ export type Variants = FeatureValue[]; /** diff --git a/flagsmith-engine/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluationResult/evaluationResult.types.ts index fc0df3b..8372d49 100644 --- a/flagsmith-engine/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluationResult/evaluationResult.types.ts @@ -112,7 +112,7 @@ export type Value3 = string | number | boolean | null; */ export type Weight = number; /** - * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. */ export type Variants = FeatureValue[]; /** diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 089b04f..576b4d2 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -21,18 +21,11 @@ import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; */ export function getIdentitySegments(context: EvaluationContext): SegmentContext[] { if (!context.identity || !context.segments) return []; - return Object.values(context.segments).filter(segment => - evaluateIdentityInSegment(segment, context) - ); -} - -export function evaluateIdentityInSegment( - segment: SegmentContext, - context?: EvaluationContext -): boolean { - if (segment.rules.length === 0) return false; - return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); + return Object.values(context.segments).filter(segment => { + if (segment.rules.length === 0) return false; + return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); + }); } /** diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 6470570..c9a201b 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -6,7 +6,6 @@ import { import { traitsMatchSegmentCondition, - evaluateIdentityInSegment, getContextValue, getIdentitySegments } from '../../../../flagsmith-engine/segments/evaluators.js'; @@ -84,7 +83,7 @@ test('test_traits_match_segment_condition_for_trait_existence_operators', () => } }); -test('evaluateIdentityInSegment uses django ID for hashed percentage when present', () => { +test('getIdentitySegments uses django ID for hashed percentage when present', () => { var identityModel = new IdentityModel( Date.now().toString(), [], @@ -117,13 +116,12 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen environmentModel.project.segments = [segmentModel]; const context = getEvaluationContext(environmentModel, identityModel); - const segmentContext = context.segments![1]; - var result = evaluateIdentityInSegment(segmentContext, context); + var result = getIdentitySegments(context); - expect(result).toBe(true); + expect(result).toHaveLength(1); expect(getHashedPercentageForObjIds).toHaveBeenCalledTimes(1); expect(getHashedPercentageForObjIds).toHaveBeenCalledWith([ - segmentContext.key, + result[0].key, context.identity!.key ]); }); @@ -297,81 +295,102 @@ describe('IN operator', () => { ); }); -describe('evaluateIdentityInSegment', () => { - const mockContext: EvaluationContext = { +describe('getIdentitySegments single segment evaluation', () => { + const baseContext: EvaluationContext = { environment: { key: 'env', name: 'test' }, identity: { key: 'user', identifier: 'test@example.com', traits: { age: 25 } }, segments: {}, features: {} }; - test('returns false for segment with no rules', () => { - const segment: SegmentContext = { - key: '1', - name: 'empty_segment', - rules: [], - overrides: [] + test('returns empty array for segment with no rules', () => { + const context = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'empty_segment', + rules: [], + overrides: [] + } + } }; - expect(evaluateIdentityInSegment(segment, mockContext)).toBe(false); + expect(getIdentitySegments(context)).toEqual([]); }); - test('returns true when all rules match', () => { - const segment: SegmentContext = { - key: '1', - name: 'matching_segment', - rules: [ - { - type: 'ALL', - conditions: [ + test('returns segment when all rules match', () => { + const context: EvaluationContext = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'matching_segment', + rules: [ { - property: '$.identity.identifier', - operator: 'EQUAL', - value: 'test@example.com' - } - ] - }, - { - type: 'ALL', - conditions: [ + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ], + rules: [] + }, { - property: '$.identity.identifier', - operator: 'CONTAINS', - value: 'test@example.com' + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'CONTAINS', + value: 'test@example.com' + } + ], + rules: [] } - ] + ], + overrides: [] } - ], - overrides: [] + } }; - expect(evaluateIdentityInSegment(segment, mockContext)).toBe(true); + const result = getIdentitySegments(context); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('matching_segment'); }); - test('returns false when any rule fails', () => { - const segment: SegmentContext = { - key: '1', - name: 'failing_segment', - rules: [ - { - type: 'ALL', - conditions: [ + test('returns empty array when any rule fails', () => { + const context: EvaluationContext = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'failing_segment', + rules: [ + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ], + rules: [] + }, { - property: '$.identity.identifier', - operator: 'EQUAL', - value: 'test@example.com' + type: ALL_RULE, + conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }], + rules: [] } - ] - }, - { - type: 'ALL', - conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }] + ], + overrides: [] } - ], - overrides: [] + } }; - expect(evaluateIdentityInSegment(segment, mockContext)).toBe(false); + expect(getIdentitySegments(context)).toEqual([]); }); }); From cdbb62c7b92b06650f4c5e4822645a7915c85907 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 1 Oct 2025 11:38:13 +0200 Subject: [PATCH 22/25] feat: improved-error-handling --- flagsmith-engine/segments/models.ts | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 98cb98f..8858ff9 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -90,18 +90,35 @@ export class SegmentConditionModel { ); }, evaluateRegex: (traitValue: any) => { - return ( - !!this.value && - !!traitValue?.toString().match(new RegExp(this.value?.toString())) - ); + try { + if (!this.value) { + return false; + } + const regex = new RegExp(this.value?.toString()); + return !!traitValue?.toString().match(regex); + } catch { + return false; + } }, evaluateModulo: (traitValue: any) => { - if (isNaN(parseFloat(traitValue)) || !this.value) { + const parsedTraitValue = parseFloat(traitValue); + if (isNaN(parsedTraitValue) || !this.value) { return false; } - const parts = this.value?.toString().split('|'); - const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])]; - return traitValue % divisor === reminder; + + const parts = this.value.toString().split('|'); + if (parts.length !== 2) { + return false; + } + + const divisor = parseFloat(parts[0]); + const remainder = parseFloat(parts[1]); + + if (isNaN(divisor) || isNaN(remainder) || divisor === 0) { + return false; + } + + return parsedTraitValue % divisor === remainder; }, evaluateIn: (traitValue: string[] | string) => { if (Array.isArray(this.value)) { From 5e039c0e4830479920d4f72f606987029646259e Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 1 Oct 2025 16:32:40 +0200 Subject: [PATCH 23/25] feat: reformatted-tests-handling-new-structure --- sdk/index.ts | 11 ++++++++++- tests/engine/e2e/engine.test.ts | 6 ++++-- tests/engine/engine-tests/engine-test-data | 2 +- tests/engine/unit/segments/segment_evaluators.test.ts | 3 +-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index 7682f2b..b543541 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -4,7 +4,7 @@ import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js' import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js'; import { BaseOfflineHandler } from './offline_handlers.js'; -import { FlagsmithAPIError } from './errors.js'; +import { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; @@ -279,6 +279,9 @@ export class Flagsmith { ); const context = getEvaluationContext(environment, identityModel); + if (!context) { + throw new FlagsmithClientError('Local evaluation required to obtain identity segments'); + } const evaluationResult = getEvaluationResult(context); return SegmentModel.fromSegmentResult(evaluationResult.segments, context); @@ -401,6 +404,9 @@ export class Flagsmith { private async getEnvironmentFlagsFromDocument(): Promise { const environment = await this.getEnvironment(); const context = getEvaluationContext(environment); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } const evaluationResult = getEvaluationResult(context); const flags = Flags.fromEvaluationResult(evaluationResult); @@ -426,6 +432,9 @@ export class Flagsmith { ); const context = getEvaluationContext(environment, identityModel); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } const evaluationResult = getEvaluationResult(context); const flags = Flags.fromEvaluationResult( diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index f565745..ff162a8 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -24,9 +24,11 @@ test('Test Engine', () => { const sortedEngineFlags = flags .allFlags() .sort((a, b) => (a.featureName > b.featureName ? 1 : -1)); - const sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) => + + const expectedFlags = testCase.response['flags'] || {}; + const sortedAPIFlags = Object.values(expectedFlags).sort((a: any, b: any) => a.name > b.name ? 1 : -1 - ); + ) as Flags[]; expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index e07cd18..c9343de 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit e07cd18b38aef93f11bd9c47e018ea01204cca25 +Subproject commit c9343de089da92f2ccb1348ab3e36e1697bc20df diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index c9a201b..6d260c7 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -18,8 +18,7 @@ import { EvaluationContext, InSegmentCondition, SegmentCondition, - SegmentCondition1, - SegmentContext + SegmentCondition1 } from '../../../../flagsmith-engine/evaluation/models.js'; // todo: work out how to implement this in a test function or before hook From 2ceb6ce913c201817bd57eed114c9aecfd6a29a2 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 3 Oct 2025 15:43:08 +0200 Subject: [PATCH 24/25] feat: renamed-and-moved-features-evaluation-to-engine --- flagsmith-engine/features/util.ts | 23 -------------- flagsmith-engine/index.ts | 35 +++++++++++++++++++++- tests/engine/engine-tests/engine-test-data | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index a02cd78..8136f0c 100644 --- a/flagsmith-engine/features/util.ts +++ b/flagsmith-engine/features/util.ts @@ -49,26 +49,3 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment { return new FeatureSegment(featureSegmentJSON.priority); } - -export function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any { - if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { - return evaluateMultivariateFeature(feature, identityKey); - } - - return feature.value; -} - -function evaluateMultivariateFeature(feature: FeatureContext, identityKey?: string): any { - const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); - - let startPercentage = 0; - for (const variant of feature?.variants || []) { - const limit = startPercentage + variant.weight; - - if (startPercentage <= percentageValue && percentageValue < limit) { - return variant.value; - } - startPercentage = limit; - } - return feature.value; -} diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 93dfc73..98a2434 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,8 +1,8 @@ import { EvaluationContext, FeatureContext } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; import { EvaluationResult, EvaluationResultFlags } from './evaluation/models.js'; -import { evaluateFeatureValue } from './features/util.js'; import { TARGETING_REASONS } from './features/types.js'; +import { getHashedPercentageForObjIds } from './utils/hashing/index.js'; export { EnvironmentModel } from './environments/models.js'; export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; @@ -124,6 +124,39 @@ export function evaluateFeatures( return flags; } +function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any { + if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { + return getMultivariateFeatureValue(feature, identityKey); + } + + return feature.value; +} + +/** + * Evaluates a multivariate feature flag to determine which variant value to return for a given identity. + * + * Uses deterministic hashing to ensure the same identity always receives the same variant, + * while distributing variants according to their configured weight percentages. + * + * @param feature - The feature context containing variants and their weights + * @param identityKey - The identity key used for deterministic variant selection + * @returns The variant value if the identity falls within a variant's range, otherwise the default feature value + */ +function getMultivariateFeatureValue(feature: FeatureContext, identityKey?: string): any { + const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); + + let startPercentage = 0; + for (const variant of feature?.variants || []) { + const limit = startPercentage + variant.weight; + + if (startPercentage <= percentageValue && percentageValue < limit) { + return variant.value; + } + startPercentage = limit; + } + return feature.value; +} + export function shouldApplyOverride( override: any, existingOverrides: Record diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index c9343de..facf33a 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit c9343de089da92f2ccb1348ab3e36e1697bc20df +Subproject commit facf33a4c50fdabdce29899b19b9ea65ea70eb18 From 9aa91b3813664a41291238ab3c32ac9993687459 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 6 Oct 2025 09:04:07 +0200 Subject: [PATCH 25/25] feat: updated-pre-commit-scripts --- .../evaluationContext.types.ts | 2 +- .../evaluationResult.types.ts | 255 +-------------- .../evaluationContext.types.ts | 233 -------------- .../evaluationResult.types.ts | 290 ------------------ package.json | 4 +- 5 files changed, 19 insertions(+), 765 deletions(-) delete mode 100644 flagsmith-engine/evaluationContext/evaluationContext.types.ts delete mode 100644 flagsmith-engine/evaluationResult/evaluationResult.types.ts diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index aef9efa..d105c96 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -113,7 +113,7 @@ export type Value3 = string | number | boolean | null; */ export type Weight = number; /** - * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. */ export type Variants = FeatureValue[]; /** diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts index fc0df3b..6644656 100644 --- a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -5,88 +5,6 @@ * and run json-schema-to-typescript to regenerate this file. */ -/** - * An environment's unique identifier. - */ -export type Key = string; -/** - * An environment's human-readable name. - */ -export type Name = string; -/** - * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. - */ -export type Identifier = string; -/** - * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. - */ -export type Key1 = string; -/** - * Key used for % split segmentation. - */ -export type Key2 = string; -/** - * The name of the segment. - */ -export type Name1 = string; -/** - * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. - */ -export type Type = 'ALL' | 'ANY' | 'NONE'; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator = - | 'EQUAL' - | 'GREATER_THAN' - | 'LESS_THAN' - | 'LESS_THAN_INCLUSIVE' - | 'CONTAINS' - | 'GREATER_THAN_INCLUSIVE' - | 'NOT_CONTAINS' - | 'NOT_EQUAL' - | 'REGEX' - | 'PERCENTAGE_SPLIT' - | 'MODULO' - | 'IS_SET' - | 'IS_NOT_SET' - | 'IN'; -/** - * The value to compare against the trait or context value. - */ -export type Value = string; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property1 = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator1 = 'IN'; -/** - * The values to compare against the trait or context value. - */ -export type Value1 = string[]; -/** - * Conditions that must be met for the rule to apply. - */ -export type Conditions = (SegmentCondition | InSegmentCondition)[]; -/** - * Sub-rules nested within the segment rule. - */ -export type SubRules = SegmentRule[]; -/** - * Rules that define the segment. - */ -export type Rules = SegmentRule[]; -/** - * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. - */ -export type Key3 = string; /** * Unique feature identifier. */ @@ -94,197 +12,56 @@ export type FeatureKey = string; /** * Feature name. */ -export type Name2 = string; -/** - * Indicates whether the feature is enabled in the environment. - */ -export type Enabled = boolean; -/** - * A default environment value for the feature. If the feature is multivariate, this will be the control value. - */ -export type Value2 = string | number | boolean | null; -/** - * The value of the feature. - */ -export type Value3 = string | number | boolean | null; -/** - * The weight of the feature value variant, as a percentage number (i.e. 100.0). - */ -export type Weight = number; -/** - * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. - */ -export type Variants = FeatureValue[]; -/** - * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. - */ -export type Priority = number; -/** - * Feature overrides for the segment. - */ -export type Overrides = FeatureContext[]; -/** - * Unique feature identifier. - */ -export type FeatureKey1 = string; -/** - * Feature name. - */ -export type Name3 = string; +export type Name = string; /** * Indicates if the feature flag is enabled. */ -export type Enabled1 = boolean; +export type Enabled = boolean; /** * Feature flag value. */ -export type Value4 = string | number | boolean | null; +export type Value = string | number | boolean | null; /** * Reason for the feature flag evaluation. */ export type Reason = string; -/** - * List of feature flags evaluated for the context. - */ -export type Flags = FlagResult[]; /** * Unique segment identifier. */ -export type Key4 = string; +export type Key = string; /** * Segment name. */ -export type Name4 = string; +export type Name1 = string; /** * List of segments which the provided context belongs to. */ -export type Segments1 = SegmentResult[]; +export type Segments = SegmentResult[]; /** * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. */ export interface EvaluationResult { - context: EvaluationContext; flags: Flags; - segments: Segments1; - [k: string]: unknown; -} -/** - * A context object containing the necessary information to evaluate Flagsmith feature flags. - */ -export interface EvaluationContext { - environment: EnvironmentContext; - /** - * Identity context used for identity-based evaluation. - */ - identity?: IdentityContext | null; - segments?: Segments; - features?: Features; - [k: string]: unknown; -} -/** - * Environment context required for evaluation. - */ -export interface EnvironmentContext { - key: Key; - name: Name; - [k: string]: unknown; -} -/** - * Represents an identity context for feature flag evaluation. - */ -export interface IdentityContext { - identifier: Identifier; - key: Key1; - traits?: Traits; - [k: string]: unknown; -} -/** - * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. - */ -export interface Traits { - [k: string]: string | number | boolean | null; -} -/** - * Segments applicable to the evaluation context. - */ -export interface Segments { - [k: string]: SegmentContext; -} -/** - * Represents a segment context for feature flag evaluation. - */ -export interface SegmentContext { - key: Key2; - name: Name1; - rules: Rules; - overrides?: Overrides; - [k: string]: unknown; -} -/** - * Represents a rule within a segment for feature flag evaluation. - */ -export interface SegmentRule { - type: Type; - conditions?: Conditions; - rules?: SubRules; + segments: Segments; [k: string]: unknown; } /** - * Represents a condition within a segment rule for feature flag evaluation. + * Feature flags evaluated for the context, mapped by feature names. */ -export interface SegmentCondition { - property: Property; - operator: Operator; - value: Value; - [k: string]: unknown; +export interface Flags { + [k: string]: FlagResult; } -/** - * Represents an IN condition within a segment rule for feature flag evaluation. - */ -export interface InSegmentCondition { - property: Property1; - operator: Operator1; - value: Value1; - [k: string]: unknown; -} -/** - * Represents a feature context for feature flag evaluation. - */ -export interface FeatureContext { - key: Key3; +export interface FlagResult { feature_key: FeatureKey; - name: Name2; + name: Name; enabled: Enabled; - value: Value2; - variants?: Variants; - priority?: Priority; - [k: string]: unknown; -} -/** - * Represents a multivariate value for a feature flag. - */ -export interface FeatureValue { - value: Value3; - weight: Weight; - [k: string]: unknown; -} -/** - * Features to be evaluated in the context. - */ -export interface Features { - [k: string]: FeatureContext; -} -export interface FlagResult { - feature_key: FeatureKey1; - name: Name3; - enabled: Enabled1; - value?: Value4; - reason?: Reason; + value: Value; + reason: Reason; [k: string]: unknown; } export interface SegmentResult { - key: Key4; - name: Name4; + key: Key; + name: Name1; [k: string]: unknown; } diff --git a/flagsmith-engine/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluationContext/evaluationContext.types.ts deleted file mode 100644 index d105c96..0000000 --- a/flagsmith-engine/evaluationContext/evaluationContext.types.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* eslint-disable */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -/** - * An environment's unique identifier. - */ -export type Key = string; -/** - * An environment's human-readable name. - */ -export type Name = string; -/** - * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. - */ -export type Identifier = string; -/** - * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. - */ -export type Key1 = string; -/** - * Key used for % split segmentation. - */ -export type Key2 = string; -/** - * The name of the segment. - */ -export type Name1 = string; -/** - * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. - */ -export type Type = 'ALL' | 'ANY' | 'NONE'; -export type SegmentCondition = SegmentCondition1 | InSegmentCondition; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator = - | 'EQUAL' - | 'GREATER_THAN' - | 'LESS_THAN' - | 'LESS_THAN_INCLUSIVE' - | 'CONTAINS' - | 'GREATER_THAN_INCLUSIVE' - | 'NOT_CONTAINS' - | 'NOT_EQUAL' - | 'REGEX' - | 'PERCENTAGE_SPLIT' - | 'MODULO' - | 'IS_SET' - | 'IS_NOT_SET' - | 'IN'; -/** - * The value to compare against the trait or context value. - */ -export type Value = string; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property1 = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator1 = 'IN'; -/** - * The values to compare against the trait or context value. - */ -export type Value1 = string[]; -/** - * Conditions that must be met for the rule to apply. - */ -export type Conditions = SegmentCondition[]; -/** - * Sub-rules nested within the segment rule. - */ -export type SubRules = SegmentRule[]; -/** - * Rules that define the segment. - */ -export type Rules = SegmentRule[]; -/** - * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. - */ -export type Key3 = string; -/** - * Unique feature identifier. - */ -export type FeatureKey = string; -/** - * Feature name. - */ -export type Name2 = string; -/** - * Indicates whether the feature is enabled in the environment. - */ -export type Enabled = boolean; -/** - * A default environment value for the feature. If the feature is multivariate, this will be the control value. - */ -export type Value2 = string | number | boolean | null; -/** - * The value of the feature. - */ -export type Value3 = string | number | boolean | null; -/** - * The weight of the feature value variant, as a percentage number (i.e. 100.0). - */ -export type Weight = number; -/** - * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. - */ -export type Variants = FeatureValue[]; -/** - * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. - */ -export type Priority = number; -/** - * Feature overrides for the segment. - */ -export type Overrides = FeatureContext[]; - -/** - * A context object containing the necessary information to evaluate Flagsmith feature flags. - */ -export interface EvaluationContext { - environment: EnvironmentContext; - /** - * Identity context used for identity-based evaluation. - */ - identity?: IdentityContext | null; - segments?: Segments; - features?: Features; - [k: string]: unknown; -} -/** - * Environment context required for evaluation. - */ -export interface EnvironmentContext { - key: Key; - name: Name; - [k: string]: unknown; -} -/** - * Represents an identity context for feature flag evaluation. - */ -export interface IdentityContext { - identifier: Identifier; - key: Key1; - traits?: Traits; - [k: string]: unknown; -} -/** - * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. - */ -export interface Traits { - [k: string]: string | number | boolean | null; -} -/** - * Segments applicable to the evaluation context. - */ -export interface Segments { - [k: string]: SegmentContext; -} -/** - * Represents a segment context for feature flag evaluation. - */ -export interface SegmentContext { - key: Key2; - name: Name1; - rules: Rules; - overrides?: Overrides; - [k: string]: unknown; -} -/** - * Represents a rule within a segment for feature flag evaluation. - */ -export interface SegmentRule { - type: Type; - conditions?: Conditions; - rules?: SubRules; - [k: string]: unknown; -} -/** - * Represents a condition within a segment rule for feature flag evaluation. - */ -export interface SegmentCondition1 { - property: Property; - operator: Operator; - value: Value; - [k: string]: unknown; -} -/** - * Represents an IN condition within a segment rule for feature flag evaluation. - */ -export interface InSegmentCondition { - property: Property1; - operator: Operator1; - value: Value1; - [k: string]: unknown; -} -/** - * Represents a feature context for feature flag evaluation. - */ -export interface FeatureContext { - key: Key3; - feature_key: FeatureKey; - name: Name2; - enabled: Enabled; - value: Value2; - variants?: Variants; - priority?: Priority; - [k: string]: unknown; -} -/** - * Represents a multivariate value for a feature flag. - */ -export interface FeatureValue { - value: Value3; - weight: Weight; - [k: string]: unknown; -} -/** - * Features to be evaluated in the context. - */ -export interface Features { - [k: string]: FeatureContext; -} diff --git a/flagsmith-engine/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluationResult/evaluationResult.types.ts deleted file mode 100644 index 8372d49..0000000 --- a/flagsmith-engine/evaluationResult/evaluationResult.types.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* eslint-disable */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -/** - * An environment's unique identifier. - */ -export type Key = string; -/** - * An environment's human-readable name. - */ -export type Name = string; -/** - * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. - */ -export type Identifier = string; -/** - * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. - */ -export type Key1 = string; -/** - * Key used for % split segmentation. - */ -export type Key2 = string; -/** - * The name of the segment. - */ -export type Name1 = string; -/** - * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. - */ -export type Type = 'ALL' | 'ANY' | 'NONE'; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator = - | 'EQUAL' - | 'GREATER_THAN' - | 'LESS_THAN' - | 'LESS_THAN_INCLUSIVE' - | 'CONTAINS' - | 'GREATER_THAN_INCLUSIVE' - | 'NOT_CONTAINS' - | 'NOT_EQUAL' - | 'REGEX' - | 'PERCENTAGE_SPLIT' - | 'MODULO' - | 'IS_SET' - | 'IS_NOT_SET' - | 'IN'; -/** - * The value to compare against the trait or context value. - */ -export type Value = string; -/** - * A reference to the identity trait or value in the evaluation context. - */ -export type Property1 = string; -/** - * The operator to use for evaluating the condition. - */ -export type Operator1 = 'IN'; -/** - * The values to compare against the trait or context value. - */ -export type Value1 = string[]; -/** - * Conditions that must be met for the rule to apply. - */ -export type Conditions = (SegmentCondition | InSegmentCondition)[]; -/** - * Sub-rules nested within the segment rule. - */ -export type SubRules = SegmentRule[]; -/** - * Rules that define the segment. - */ -export type Rules = SegmentRule[]; -/** - * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. - */ -export type Key3 = string; -/** - * Unique feature identifier. - */ -export type FeatureKey = string; -/** - * Feature name. - */ -export type Name2 = string; -/** - * Indicates whether the feature is enabled in the environment. - */ -export type Enabled = boolean; -/** - * A default environment value for the feature. If the feature is multivariate, this will be the control value. - */ -export type Value2 = string | number | boolean | null; -/** - * The value of the feature. - */ -export type Value3 = string | number | boolean | null; -/** - * The weight of the feature value variant, as a percentage number (i.e. 100.0). - */ -export type Weight = number; -/** - * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. - */ -export type Variants = FeatureValue[]; -/** - * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. - */ -export type Priority = number; -/** - * Feature overrides for the segment. - */ -export type Overrides = FeatureContext[]; -/** - * Unique feature identifier. - */ -export type FeatureKey1 = string; -/** - * Feature name. - */ -export type Name3 = string; -/** - * Indicates if the feature flag is enabled. - */ -export type Enabled1 = boolean; -/** - * Feature flag value. - */ -export type Value4 = string | number | boolean | null; -/** - * Reason for the feature flag evaluation. - */ -export type Reason = string; -/** - * List of feature flags evaluated for the context. - */ -export type Flags = FlagResult[]; -/** - * Unique segment identifier. - */ -export type Key4 = string; -/** - * Segment name. - */ -export type Name4 = string; -/** - * List of segments which the provided context belongs to. - */ -export type Segments1 = SegmentResult[]; - -/** - * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. - */ -export interface EvaluationResult { - context: EvaluationContext; - flags: Flags; - segments: Segments1; - [k: string]: unknown; -} -/** - * A context object containing the necessary information to evaluate Flagsmith feature flags. - */ -export interface EvaluationContext { - environment: EnvironmentContext; - /** - * Identity context used for identity-based evaluation. - */ - identity?: IdentityContext | null; - segments?: Segments; - features?: Features; - [k: string]: unknown; -} -/** - * Environment context required for evaluation. - */ -export interface EnvironmentContext { - key: Key; - name: Name; - [k: string]: unknown; -} -/** - * Represents an identity context for feature flag evaluation. - */ -export interface IdentityContext { - identifier: Identifier; - key: Key1; - traits?: Traits; - [k: string]: unknown; -} -/** - * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. - */ -export interface Traits { - [k: string]: string | number | boolean | null; -} -/** - * Segments applicable to the evaluation context. - */ -export interface Segments { - [k: string]: SegmentContext; -} -/** - * Represents a segment context for feature flag evaluation. - */ -export interface SegmentContext { - key: Key2; - name: Name1; - rules: Rules; - overrides?: Overrides; - [k: string]: unknown; -} -/** - * Represents a rule within a segment for feature flag evaluation. - */ -export interface SegmentRule { - type: Type; - conditions?: Conditions; - rules?: SubRules; - [k: string]: unknown; -} -/** - * Represents a condition within a segment rule for feature flag evaluation. - */ -export interface SegmentCondition { - property: Property; - operator: Operator; - value: Value; - [k: string]: unknown; -} -/** - * Represents an IN condition within a segment rule for feature flag evaluation. - */ -export interface InSegmentCondition { - property: Property1; - operator: Operator1; - value: Value1; - [k: string]: unknown; -} -/** - * Represents a feature context for feature flag evaluation. - */ -export interface FeatureContext { - key: Key3; - feature_key: FeatureKey; - name: Name2; - enabled: Enabled; - value: Value2; - variants?: Variants; - priority?: Priority; - [k: string]: unknown; -} -/** - * Represents a multivariate value for a feature flag. - */ -export interface FeatureValue { - value: Value3; - weight: Weight; - [k: string]: unknown; -} -/** - * Features to be evaluated in the context. - */ -export interface Features { - [k: string]: FeatureContext; -} -export interface FlagResult { - feature_key: FeatureKey1; - name: Name3; - enabled: Enabled1; - value?: Value4; - reason?: Reason; - [k: string]: unknown; -} -export interface SegmentResult { - key: Key4; - name: Name4; - [k: string]: unknown; -} diff --git a/package.json b/package.json index 2e81194..ec45f87 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "deploy": "npm i && npm run build && npm publish", "deploy:beta": "npm i && npm run build && npm publish --tag beta", "prepare": "husky install", - "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json", - "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json", + "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json", + "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json", "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types" }, "dependencies": {