From 4040befbc91fa851041f1a8dfbd433a27a9f64da Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 18 Sep 2025 17:45:00 +0200 Subject: [PATCH 01/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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": { From f1ee3ef31005d769491292879612ccfd36754945 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 8 Oct 2025 11:24:23 +0200 Subject: [PATCH 26/43] feat: updated-case-extraction-to-use-json-and-separate-each-run --- flagsmith-engine/segments/models.ts | 38 +- package-lock.json | 2644 +++++++++++++++++ package.json | 3 + tests/engine/e2e/engine.test.ts | 87 +- tests/engine/engine-tests/engine-test-data | 2 +- .../unit/segments/segments_model.test.ts | 1 + 6 files changed, 2732 insertions(+), 43 deletions(-) diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 8858ff9..ee8a8c7 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -41,18 +41,36 @@ export const matchingFunctions = { !!otherValue && otherValue.includes(thisValue) }; +// Semver library throws an error if the version is invalid, in this case, we want to catch and return false +const safeSemverCompare = ( + semverMatchingFunction: (conditionValue: any, traitValue: any) => boolean +) => { + return (conditionValue: any, traitValue: any) => { + try { + return semverMatchingFunction(conditionValue, traitValue); + } catch { + return false; + } + }; +}; + export const semverMatchingFunction = { ...matchingFunctions, - [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => - semver.eq(thisValue, otherValue), - [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => - semver.gt(otherValue, thisValue), - [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => - semver.gte(otherValue, thisValue), - [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => - semver.gt(thisValue, otherValue), - [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => - semver.gte(thisValue, otherValue) + [CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) => + semver.eq(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) => + semver.gt(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => + semver.gte(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) => + semver.lt(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => + semver.lte(traitValue, conditionValue) + ) }; export const getMatchingFunctions = (semver: boolean) => diff --git a/package-lock.json b/package-lock.json index 330811c..933acba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,10 @@ "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "install": "^0.13.0", "json-schema-to-typescript": "^15.0.4", + "jsonc-parser": "^3.3.1", + "npm": "^11.6.1", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", @@ -2332,6 +2335,16 @@ } ] }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2601,6 +2614,13 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonpath": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", @@ -2762,6 +2782,2630 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/npm": { + "version": "11.6.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.1.tgz", + "integrity": "sha512-7iDSHDoup6uMQJ37yWrhfqcbMhF0UEfGRap6Nv+aKQcrIJXlCi2cKbj75WBmiHlcwsQCy/U0zEwDZdAx6H/Vaw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "dev": true, + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.1.5", + "@npmcli/config": "^10.4.1", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.1", + "@npmcli/promise-spawn": "^8.0.3", + "@npmcli/redact": "^3.2.2", + "@npmcli/run-script": "^10.0.0", + "@sigstore/tuf": "^4.0.0", + "abbrev": "^3.0.1", + "archy": "~1.0.0", + "cacache": "^20.0.1", + "chalk": "^5.6.2", + "ci-info": "^4.3.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^11.0.3", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^9.0.0", + "ini": "^5.0.0", + "init-package-json": "^8.2.2", + "is-cidr": "^6.0.0", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^10.0.2", + "libnpmdiff": "^8.0.8", + "libnpmexec": "^10.1.7", + "libnpmfund": "^7.0.8", + "libnpmorg": "^8.0.1", + "libnpmpack": "^9.0.8", + "libnpmpublish": "^11.1.1", + "libnpmsearch": "^9.0.1", + "libnpmteam": "^8.0.2", + "libnpmversion": "^8.0.2", + "make-fetch-happen": "^15.0.2", + "minimatch": "^10.0.3", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.4.2", + "nopt": "^8.1.0", + "normalize-package-data": "^8.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.2", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-profile": "^12.0.0", + "npm-registry-fetch": "^19.0.0", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^21.0.3", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.1.0", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^10.2.2", + "tar": "^7.5.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^2.0.2", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.2", + "which": "^5.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^5.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.4.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.1.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^11.0.3", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^11.0.3", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "10.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "20.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "11.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "npm-package-arg": "^13.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.7.2", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^3.0.0", + "diff": "^8.0.2", + "minimatch": "^10.0.3", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "tar": "^7.5.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.1.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/package-json": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "read": "^4.0.0", + "semver": "^7.3.7", + "signal-exit": "^4.1.0", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/run-script": "^10.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^4.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "11.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "15.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "10.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.4.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "dev": true, + "inBundle": 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/npm/node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "14.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "13.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "19.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^12.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.22", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "10.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "7.5.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "inBundle": 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/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.0.0", + "debug": "^4.4.1", + "make-fetch-happen": "^15.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", diff --git a/package.json b/package.json index ec45f87..ede37a6 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,10 @@ "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "install": "^0.13.0", "json-schema-to-typescript": "^15.0.4", + "jsonc-parser": "^3.3.1", + "npm": "^11.6.1", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index ff162a8..6be9568 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -1,40 +1,63 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; 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'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; +import { parse as parseJsonc } from 'jsonc-parser'; -function extractTestCases(data: any): { - response: any; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const TEST_DATA_DIR = path.join(__dirname, '../engine-tests/engine-test-data/test_cases'); +interface TestCase { context: EvaluationContext; -}[] { - const test_data = data['test_cases'].map((test_case: any) => { - return { - context: test_case['context'], - response: test_case['result'] - }; - }); - return test_data; + result: { + flags?: Record; + }; +} + +function getTestFiles(): string[] { + const files = fs.readdirSync(TEST_DATA_DIR); + return files + .filter(f => f.endsWith('.json') || f.endsWith('.jsonc')) + .map(f => path.join(TEST_DATA_DIR, f)); } -test('Test Engine', () => { - const testCases = extractTestCases(testData); - for (const testCase of testCases) { - 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 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); - - for (let i = 0; i < sortedEngineFlags.length; i++) { - expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i].value); - expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i]['enabled']); - } +function loadTestFile(filePath: string): TestCase { + const content = fs.readFileSync(filePath, 'utf-8'); + return parseJsonc(content); +} + +describe('Engine Integration Tests', () => { + const testFiles = getTestFiles(); + + if (testFiles.length === 0) { + throw new Error(`No test files found in ${TEST_DATA_DIR}`); } + + testFiles.forEach(filePath => { + const testName = path.basename(filePath, path.extname(filePath)); + + test(testName, () => { + const testCase = loadTestFile(filePath); + + 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 expectedFlags = testCase.result.flags || {}; + const sortedAPIFlags = Object.values(expectedFlags).sort((a: any, b: any) => + a.name > b.name ? 1 : -1 + ) as any[]; + + expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); + + for (let i = 0; i < sortedEngineFlags.length; i++) { + 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 facf33a..f32e8ee 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit facf33a4c50fdabdce29899b19b9ea65ea70eb18 +Subproject commit f32e8eeb2fc7a08bc9d1e8a18c3e4c241f0ce81f diff --git a/tests/engine/unit/segments/segments_model.test.ts b/tests/engine/unit/segments/segments_model.test.ts index 5607f03..89982b6 100644 --- a/tests/engine/unit/segments/segments_model.test.ts +++ b/tests/engine/unit/segments/segments_model.test.ts @@ -107,6 +107,7 @@ const conditionMatchCases: [string, string | number | boolean | null, string, bo test('test_segment_condition_matches_trait_value', () => { for (const testCase of conditionMatchCases) { const [operator, traitValue, conditionValue, expectedResult] = testCase; + console.log(operator, traitValue, conditionValue, expectedResult); expect( new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(traitValue) ).toBe(expectedResult); From 45c45949b8f81f85a8f77bf8c089c058acbe6cd7 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Oct 2025 11:15:45 +0200 Subject: [PATCH 27/43] feat: added-reasons-to-flag-object --- flagsmith-engine/features/types.ts | 3 +- flagsmith-engine/index.ts | 60 ++++++++++++++----- sdk/models.ts | 11 +++- tests/engine/e2e/engine.test.ts | 1 + .../unit/segments/segments_model.test.ts | 1 - 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/flagsmith-engine/features/types.ts b/flagsmith-engine/features/types.ts index 8f7fad1..f792e2d 100644 --- a/flagsmith-engine/features/types.ts +++ b/flagsmith-engine/features/types.ts @@ -1,4 +1,5 @@ export enum TARGETING_REASONS { DEFAULT = 'DEFAULT', - TARGETING_MATCH = 'TARGETING_MATCH' + TARGETING_MATCH = 'TARGETING_MATCH', + SPLIT = 'SPLIT' } diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 98a2434..a40bbf4 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -108,28 +108,34 @@ export function evaluateFeatures( const segmentOverride = segmentOverrides[feature.feature_key]; const finalFeature = segmentOverride ? segmentOverride.feature : feature; const hasOverride = !!segmentOverride; - const reason = getTargetingMatchReason(segmentOverride); + + const { value: evaluatedValue, reason: evaluatedReason } = hasOverride + ? { value: finalFeature.value, reason: undefined } + : evaluateFeatureValue(finalFeature, context.identity?.key); flags.push({ feature_key: finalFeature.feature_key, name: finalFeature.name, enabled: finalFeature.enabled, - value: hasOverride - ? finalFeature.value - : evaluateFeatureValue(finalFeature, context.identity?.key), - reason + value: evaluatedValue, + reason: + evaluatedReason ?? + getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) }); } return flags; } -function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any { +function evaluateFeatureValue( + feature: FeatureContext, + identityKey?: string +): { value: any; reason?: string } { if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { return getMultivariateFeatureValue(feature, identityKey); } - return feature.value; + return { value: feature.value, reason: undefined }; } /** @@ -142,7 +148,10 @@ function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): an * @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 { +function getMultivariateFeatureValue( + feature: FeatureContext, + identityKey?: string +): { value: any; reason?: string } { const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); let startPercentage = 0; @@ -150,11 +159,14 @@ function getMultivariateFeatureValue(feature: FeatureContext, identityKey?: stri const limit = startPercentage + variant.weight; if (startPercentage <= percentageValue && percentageValue < limit) { - return variant.value; + return { + value: variant.value, + reason: getTargetingMatchReason({ type: 'SPLIT', weight: variant.weight }) + }; } startPercentage = limit; } - return feature.value; + return { value: feature.value, reason: undefined }; } export function shouldApplyOverride( @@ -174,8 +186,28 @@ export function isHigherPriority( return (priorityA ?? Infinity) < (priorityB ?? Infinity); } -const getTargetingMatchReason = (segmentOverride: SegmentOverride) => { - return segmentOverride - ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}` - : TARGETING_REASONS.DEFAULT; +export type TargetingMatchReason = + | { + type: 'SEGMENT'; + override: SegmentOverride; + } + | { + type: 'SPLIT'; + weight: number; + }; + +const getTargetingMatchReason = (matchObject: TargetingMatchReason) => { + const { type } = matchObject; + + if (type === 'SEGMENT') { + return matchObject.override + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${matchObject.override.segmentName}` + : TARGETING_REASONS.DEFAULT; + } + + if (type === 'SPLIT') { + return `${TARGETING_REASONS.SPLIT}; weight=${matchObject.weight}`; + } + + return TARGETING_REASONS.DEFAULT; }; diff --git a/sdk/models.ts b/sdk/models.ts index 2fdfbed..e478ded 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -50,6 +50,10 @@ export class Flag extends BaseFlag { * The programmatic name for this feature, unique per Flagsmith project. */ featureName: string; + /** + * The reason for this feature, unique per Flagsmith project. + */ + reason?: string; constructor(params: { value: FlagValue; @@ -62,6 +66,7 @@ export class Flag extends BaseFlag { super(params.value, params.enabled, !!params.isDefault); this.featureId = params.featureId; this.featureName = params.featureName; + this.reason = params.reason; } static fromFeatureStateModel( @@ -81,7 +86,8 @@ export class Flag extends BaseFlag { enabled: flagResult.enabled, value: flagResult.value ?? null, featureId: Number(flagResult.feature_key), - featureName: flagResult.name + featureName: flagResult.name, + reason: flagResult.reason }); } @@ -90,7 +96,8 @@ export class Flag extends BaseFlag { enabled: flagData['enabled'], value: flagData['feature_state_value'] ?? flagData['value'], featureId: flagData['feature']['id'], - featureName: flagData['feature']['name'] + featureName: flagData['feature']['name'], + reason: flagData['feature']['reason'] }); } } diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index 6be9568..70a32e2 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -57,6 +57,7 @@ describe('Engine Integration Tests', () => { for (let i = 0; i < sortedEngineFlags.length; i++) { expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i].value); expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i].enabled); + expect(sortedEngineFlags[i].reason).toBe(sortedAPIFlags[i].reason); } }); }); diff --git a/tests/engine/unit/segments/segments_model.test.ts b/tests/engine/unit/segments/segments_model.test.ts index 89982b6..5607f03 100644 --- a/tests/engine/unit/segments/segments_model.test.ts +++ b/tests/engine/unit/segments/segments_model.test.ts @@ -107,7 +107,6 @@ const conditionMatchCases: [string, string | number | boolean | null, string, bo test('test_segment_condition_matches_trait_value', () => { for (const testCase of conditionMatchCases) { const [operator, traitValue, conditionValue, expectedResult] = testCase; - console.log(operator, traitValue, conditionValue, expectedResult); expect( new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(traitValue) ).toBe(expectedResult); From 10621adca51248987656cabc775206e09c211ed8 Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 10 Oct 2025 11:13:51 +0200 Subject: [PATCH 28/43] feat: fix-get-identity-segments-returning-identity-overrides --- flagsmith-engine/evaluation/evaluationContext/mappers.ts | 2 +- flagsmith-engine/evaluation/models.ts | 4 ++-- flagsmith-engine/index.ts | 3 ++- flagsmith-engine/segments/models.ts | 5 ++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index a77b4de..78f501d 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -151,6 +151,7 @@ function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Seg featuresToIdentifiers.get(overridesHash)!.identifiers.push(identity.identifier); } + for (const [overrideHash, { identifiers, overrides }] of featuresToIdentifiers.entries()) { const segmentKey = `identity_override_${overrideHash}`; @@ -164,7 +165,6 @@ function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Seg { property: '$.identity.identifier', operator: 'IN', - // TODO: Modify once new IN operator is implemented value: identifiers.join(',') } ] diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index ba9c511..25362aa 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -68,5 +68,5 @@ export type EvaluationResult = { segments: EvaluationResultSegments; }; -export type { FlagResult } from './evaluationResult/evaluationResult.types.js'; -export type * from './evaluationContext/evaluationContext.types.js'; +export { FlagResult } from './evaluationResult/evaluationResult.types.js'; +export * from './evaluationContext/evaluationContext.types.js'; diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index a40bbf4..cedcb79 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -7,7 +7,8 @@ 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'; - +export { FeatureModel, FeatureStateModel } from './features/models.js'; +export { OrganisationModel } from './organisations/models.js'; type SegmentOverride = { feature: FeatureContext; segmentName: string; diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index ee8a8c7..9c278e6 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -223,7 +223,10 @@ export class SegmentModel { segment.featureStates = SegmentModel.createFeatureStatesFromOverrides( segmentContext.overrides || [] ); - segmentModels.push(segment); + + if (!isNaN(segment.id)) { + segmentModels.push(segment); + } } } From d9f4b63fff81895e2860a9b50f101f65c3e9647c Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 13 Oct 2025 16:24:17 +0200 Subject: [PATCH 29/43] feat: fixed-tests --- .../evaluationContext.types.ts | 7 +++++ .../evaluation/evaluationContext/mappers.ts | 12 +++++++-- .../evaluationResult.types.ts | 7 +++++ flagsmith-engine/evaluation/models.ts | 27 ++++++++++++------- flagsmith-engine/index.ts | 20 +++++++++----- flagsmith-engine/segments/models.ts | 10 +++---- sdk/models.ts | 2 +- tests/engine/e2e/engine.test.ts | 4 ++- tests/engine/engine-tests/engine-test-data | 2 +- tests/engine/unit/engine.test.ts | 18 ++++++------- 10 files changed, 74 insertions(+), 35 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index d105c96..31af023 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -175,6 +175,7 @@ export interface SegmentContext { name: Name1; rules: Rules; overrides?: Overrides; + metadata?: Metadata; [k: string]: unknown; } /** @@ -225,6 +226,12 @@ export interface FeatureValue { weight: Weight; [k: string]: unknown; } +/** + * Additional metadata associated with the segment. + */ +export interface Metadata { + [k: string]: string | number | boolean | null; +} /** * Features to be evaluated in the context. */ diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 78f501d..5907045 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -4,7 +4,8 @@ import { Traits, EvaluationContext, EnvironmentContext, - IdentityContext + IdentityContext, + SegmentSource } from '../models.js'; import { EnvironmentModel } from '../../environments/models.js'; import { IdentityModel } from '../../identities/models.js'; @@ -74,7 +75,11 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): value: fs.getValue(), priority: fs.featureSegment?.priority })) - : [] + : [], + metadata: { + source: SegmentSource.API, + flagsmith_id: segment.id + } }; } @@ -170,6 +175,9 @@ function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Seg ] } ], + metadata: { + source: SegmentSource.IDENTITY_OVERRIDE + }, overrides: overrides }; } diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts index 6644656..306ddb3 100644 --- a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -63,5 +63,12 @@ export interface FlagResult { export interface SegmentResult { key: Key; name: Name1; + metadata?: Metadata; [k: string]: unknown; } +/** + * Additional metadata associated with the segment. + */ +export interface Metadata { + [k: string]: string | number | boolean | null; +} diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index 25362aa..334444b 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -13,8 +13,7 @@ import type { FeatureValue as ContextFeatureValue, Traits, Features, - Segments, - EvaluationContext + Segments } from './evaluationContext/evaluationContext.types.js'; import type { @@ -54,19 +53,27 @@ 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 EvaluationResultFlags = Record< + string, + { + feature_key: FeatureKey; + name: FeatureName; + enabled: FeatureEnabled; + value: FeatureValue; + reason: EvaluationReason; + } +>; export type EvaluationResult = { - context: EvaluationContext; flags: EvaluationResultFlags; segments: EvaluationResultSegments; }; export { FlagResult } from './evaluationResult/evaluationResult.types.js'; + +export enum SegmentSource { + API = 'api', + IDENTITY_OVERRIDE = 'identity_override' +} + export * from './evaluationContext/evaluationContext.types.js'; diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index cedcb79..a3bd68a 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,4 +1,4 @@ -import { EvaluationContext, FeatureContext } from './evaluation/models.js'; +import { EvaluationContext, FeatureContext, SegmentSource } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; import { EvaluationResult, EvaluationResultFlags } from './evaluation/models.js'; import { TARGETING_REASONS } from './features/types.js'; @@ -9,6 +9,7 @@ export { TraitModel } from './identities/traits/models.js'; export { SegmentModel } from './segments/models.js'; export { FeatureModel, FeatureStateModel } from './features/models.js'; export { OrganisationModel } from './organisations/models.js'; + type SegmentOverride = { feature: FeatureContext; segmentName: string; @@ -30,7 +31,7 @@ export function getEvaluationResult(context: EvaluationContext): EvaluationResul const { segments, segmentOverrides } = evaluateSegments(context); const flags = evaluateFeatures(context, segmentOverrides); - return { context, flags, segments }; + return { flags, segments }; } /** @@ -50,7 +51,14 @@ export function evaluateSegments(context: EvaluationContext): { const segments = identitySegments.map(segment => ({ key: segment.key, - name: segment.name + name: segment.name, + ...(segment.metadata + ? { + metadata: { + ...segment.metadata + } + } + : {}) })); const segmentOverrides = processSegmentOverrides(identitySegments); @@ -103,7 +111,7 @@ export function evaluateFeatures( context: EvaluationContext, segmentOverrides: Record ): EvaluationResultFlags { - const flags: EvaluationResultFlags = []; + const flags: EvaluationResultFlags = {}; for (const feature of Object.values(context.features || {})) { const segmentOverride = segmentOverrides[feature.feature_key]; @@ -114,7 +122,7 @@ export function evaluateFeatures( ? { value: finalFeature.value, reason: undefined } : evaluateFeatureValue(finalFeature, context.identity?.key); - flags.push({ + flags[finalFeature.name] = { feature_key: finalFeature.feature_key, name: finalFeature.name, enabled: finalFeature.enabled, @@ -122,7 +130,7 @@ export function evaluateFeatures( reason: evaluatedReason ?? getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) - }); + }; } return flags; diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 9c278e6..f050111 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -23,7 +23,7 @@ import { Overrides } from '../evaluation/evaluationContext/evaluationContext.types.js'; import { CONSTANTS } from '../features/constants.js'; -import { EvaluationResultSegments } from '../evaluation/models.js'; +import { EvaluationResultSegments, SegmentSource } 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; @@ -216,6 +216,9 @@ export class SegmentModel { } for (const segmentResult of segmentResults) { + if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) { + continue; + } const segmentContext = evaluationContext.segments[segmentResult.key]; if (segmentContext) { const segment = new SegmentModel(parseInt(segmentContext.key), segmentContext.name); @@ -223,10 +226,7 @@ export class SegmentModel { segment.featureStates = SegmentModel.createFeatureStatesFromOverrides( segmentContext.overrides || [] ); - - if (!isNaN(segment.id)) { - segmentModels.push(segment); - } + segmentModels.push(segment); } } diff --git a/sdk/models.ts b/sdk/models.ts index e478ded..e7ff613 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -123,7 +123,7 @@ export class Flags { analyticsProcessor?: AnalyticsProcessor ): Flags { const flags: { [key: string]: Flag } = {}; - for (const flag of evaluationResult.flags) { + for (const flag of Object.values(evaluationResult.flags)) { flags[flag.name] = new Flag({ enabled: flag.enabled, value: flag.value ?? null, diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index 70a32e2..7cf9df2 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -13,6 +13,7 @@ interface TestCase { context: EvaluationContext; result: { flags?: Record; + segments?: Record; }; } @@ -53,7 +54,8 @@ describe('Engine Integration Tests', () => { ) as any[]; expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); - + expect(engine_response.segments).toStrictEqual(testCase.result.segments); + expect(engine_response.flags).toStrictEqual(testCase.result.flags); for (let i = 0; i < sortedEngineFlags.length; i++) { 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 f32e8ee..37606e4 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit f32e8eeb2fc7a08bc9d1e8a18c3e4c241f0ce81f +Subproject commit 37606e4437d1bd0ee6d86d79828c70a46e94fc8e diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 1d6df05..8eb7d5d 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -27,7 +27,7 @@ test('test_get_evaluation_result_without_any_override', () => { const context = getEvaluationContext(environment(), identity()); const result = getEvaluationResult(context); - const flag = result.flags.find(f => f.name === feature1().name); + const flag = Object.values(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()); @@ -46,9 +46,9 @@ test('test_get_evaluation_result_with_identity_override_and_no_segment_override' const context = getEvaluationContext(env, ident); const result = getEvaluationResult(context); - expect(result.flags.length).toBe(3); + expect(Object.keys(result.flags).length).toBe(3); - for (const flag of result.flags) { + for (const flag of Object.values(result.flags)) { const environmentFeature = Object.values(context.features || {}).find( f => f.name === flag.name ); @@ -73,7 +73,7 @@ test('test_identity_get_all_feature_states_with_traits', () => { const result = getEvaluationResult(context); - const overriddenFlag = result.flags.find(f => f.value === 'segment_override'); + const overriddenFlag = Object.values(result.flags).find(f => f.value === 'segment_override'); expect(overriddenFlag).toBeDefined(); expect(overriddenFlag?.value).toBe('segment_override'); expect(overriddenFlag?.reason).toEqual( @@ -86,13 +86,13 @@ test('test_environment_get_all_feature_states', () => { const context = getEvaluationContext(env); const result = getEvaluationResult(context); - expect(result.flags.length).toBe(Object.keys(context.features || {}).length); + expect(Object.keys(result.flags).length).toBe(Object.keys(context.features || {}).length); - result.flags.forEach(flag => { + Object.values(result.flags).forEach(flag => { expect(flag.reason).toBe(TARGETING_REASONS.DEFAULT); }); - for (const flag of result.flags) { + for (const flag of Object.values(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); @@ -362,6 +362,6 @@ test('evaluateFeatures with multivariate evaluation', () => { } }; - const result = evaluateFeatures(context, {}); - expect(result[0].value).toBe('variant_b'); + const flags = evaluateFeatures(context, {}); + expect(flags['Multivariate Feature'].value).toBe('variant_b'); }); From 57060788f04dc2c658143cfd47da643beb52b45e Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 14 Oct 2025 15:01:35 +0200 Subject: [PATCH 30/43] feat: use-mv-fs-uuid-fallback-for-variant-sorting --- flagsmith-engine/evaluation/evaluationContext/mappers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 5907045..32337a7 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -42,7 +42,7 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): const variants = fs.multivariateFeatureStateValues.length > 0 ? [...fs.multivariateFeatureStateValues] - .sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) + .sort((a, b) => (a.id ?? a.mvFsValueUuid) - (b.id ?? b.mvFsValueUuid)) .map(mv => ({ value: mv.multivariateFeatureOption.value, weight: mv.percentageAllocation From f5b4c4621793a62ef1524a846f47d0461288c044 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 15 Oct 2025 09:48:29 +0200 Subject: [PATCH 31/43] feat: added-priority-to-variant-evaluation --- .../evaluationContext/evaluationContext.types.ts | 11 ++++++++--- .../evaluation/evaluationContext/mappers.ts | 5 +++-- .../evaluationResult/evaluationResult.types.ts | 2 +- flagsmith-engine/index.ts | 7 +++++-- tests/engine/engine-tests/engine-test-data | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index 31af023..801d8d9 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -112,6 +112,10 @@ 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; +/** + * Priority of the feature flag variant. Lower values indicate a higher priority when multiple variants apply to the same context key. + */ +export type Priority = number; /** * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. */ @@ -119,7 +123,7 @@ 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; +export type Priority1 = number; /** * Feature overrides for the segment. */ @@ -215,7 +219,7 @@ export interface FeatureContext { enabled: Enabled; value: Value2; variants?: Variants; - priority?: Priority; + priority?: Priority1; [k: string]: unknown; } /** @@ -224,13 +228,14 @@ export interface FeatureContext { export interface FeatureValue { value: Value3; weight: Weight; + priority: Priority; [k: string]: unknown; } /** * Additional metadata associated with the segment. */ export interface Metadata { - [k: string]: string | number | boolean | null; + [k: string]: unknown; } /** * Features to be evaluated in the context. diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 32337a7..bcd19ac 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -43,9 +43,10 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): fs.multivariateFeatureStateValues.length > 0 ? [...fs.multivariateFeatureStateValues] .sort((a, b) => (a.id ?? a.mvFsValueUuid) - (b.id ?? b.mvFsValueUuid)) - .map(mv => ({ + .map((mv, index) => ({ value: mv.multivariateFeatureOption.value, - weight: mv.percentageAllocation + weight: mv.percentageAllocation, + priority: index })) : undefined; features[fs.feature.name] = { diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts index 306ddb3..796db1b 100644 --- a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -70,5 +70,5 @@ export interface SegmentResult { * Additional metadata associated with the segment. */ export interface Metadata { - [k: string]: string | number | boolean | null; + [k: string]: unknown; } diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index a3bd68a..9eaa814 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -162,11 +162,13 @@ function getMultivariateFeatureValue( identityKey?: string ): { value: any; reason?: string } { const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); + const sortedVariants = [...(feature?.variants || [])].sort((a, b) => { + return (a.priority ?? Infinity) - (b.priority ?? Infinity); + }); let startPercentage = 0; - for (const variant of feature?.variants || []) { + for (const variant of sortedVariants) { const limit = startPercentage + variant.weight; - if (startPercentage <= percentageValue && percentageValue < limit) { return { value: variant.value, @@ -175,6 +177,7 @@ function getMultivariateFeatureValue( } startPercentage = limit; } + return { value: feature.value, reason: undefined }; } diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 22ab978..3d26dc5 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 22ab978fcea68cb67b261fdc8435fa2a921fe50e +Subproject commit 3d26dc53a706880e79af28903fae454657f3be50 From dc79eeb698fa45ae7b12edebb2f33e13b058e881 Mon Sep 17 00:00:00 2001 From: wadii Date: Wed, 15 Oct 2025 09:49:50 +0200 Subject: [PATCH 32/43] feat: synced-submodule-with-main --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index d7c6389..8f1b9ac 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 = feat/context-values-intensifies + branch = main From 10205ba81e9aa13ca051ad2df7f6962ed17f6a96 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Oct 2025 10:58:14 +0200 Subject: [PATCH 33/43] feat: map-priority-based-on-ids --- .../evaluation/evaluationContext/mappers.ts | 17 +++++++++-------- flagsmith-engine/features/util.ts | 7 ++++--- tests/engine/engine-tests/engine-test-data | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index bcd19ac..c374aa0 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -12,6 +12,8 @@ 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'; +import { parse as uuidParse } from 'uuid'; +import { uuidToBigInt } from '../../features/util.js'; export function getEvaluationContext( environment: EnvironmentModel, @@ -40,15 +42,14 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): const features: Features = {}; for (const fs of environment.featureStates) { const variants = - fs.multivariateFeatureStateValues.length > 0 - ? [...fs.multivariateFeatureStateValues] - .sort((a, b) => (a.id ?? a.mvFsValueUuid) - (b.id ?? b.mvFsValueUuid)) - .map((mv, index) => ({ - value: mv.multivariateFeatureOption.value, - weight: mv.percentageAllocation, - priority: index - })) + fs.multivariateFeatureStateValues?.length > 0 + ? fs.multivariateFeatureStateValues.map(mv => ({ + value: mv.multivariateFeatureOption.value, + weight: mv.percentageAllocation, + priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid) + })) : undefined; + features[fs.feature.name] = { key: fs.djangoID?.toString() || fs.featurestateUUID, feature_key: fs.feature.id.toString(), diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index 8136f0c..ef7224d 100644 --- a/flagsmith-engine/features/util.ts +++ b/flagsmith-engine/features/util.ts @@ -6,9 +6,6 @@ import { MultivariateFeatureStateValueModel } from './models.js'; -import { FeatureContext } from '../evaluation/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); } @@ -49,3 +46,7 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment { return new FeatureSegment(featureSegmentJSON.priority); } + +export function uuidToBigInt(uuid: string): BigInt { + return BigInt('0x' + uuid.replace(/-/g, '')); +} diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 3d26dc5..6453b03 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 3d26dc53a706880e79af28903fae454657f3be50 +Subproject commit 6453b0391344a4d677a97cc4a9d27a8b8e329787 From e0225898638dd7e27331d5ca9d5dd65d25c5b680 Mon Sep 17 00:00:00 2001 From: Zaimwa9 Date: Thu, 16 Oct 2025 12:00:25 +0200 Subject: [PATCH 34/43] Update flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts Co-authored-by: Kim Gustyr --- .../evaluation/evaluationContext/evaluationContext.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index 801d8d9..e4632a1 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -219,7 +219,7 @@ export interface FeatureContext { enabled: Enabled; value: Value2; variants?: Variants; - priority?: Priority1; + priority: Priority1; [k: string]: unknown; } /** From b7cf1435b38a5a5a97671c1678c8c3cde60ee748 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Oct 2025 12:16:19 +0200 Subject: [PATCH 35/43] feat/regenerated-types --- .../evaluation/evaluationContext/evaluationContext.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index e4632a1..801d8d9 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -219,7 +219,7 @@ export interface FeatureContext { enabled: Enabled; value: Value2; variants?: Variants; - priority: Priority1; + priority?: Priority1; [k: string]: unknown; } /** From 1e061fb15490a5f5966ae16d6da2f95208ea9ed4 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Oct 2025 14:14:57 +0200 Subject: [PATCH 36/43] feat: simplified-tests --- .gitmodules | 2 +- .../evaluationContext.types.ts | 11 ++++++-- .../evaluationResult.types.ts | 11 ++++++-- tests/engine/e2e/engine.test.ts | 26 +++---------------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8f1b9ac..0857b52 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 = main + branch = v2.4.0 diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index 801d8d9..6d04604 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -179,7 +179,7 @@ export interface SegmentContext { name: Name1; rules: Rules; overrides?: Overrides; - metadata?: Metadata; + metadata?: Metadata1; [k: string]: unknown; } /** @@ -220,6 +220,7 @@ export interface FeatureContext { value: Value2; variants?: Variants; priority?: Priority1; + metadata?: Metadata; [k: string]: unknown; } /** @@ -232,11 +233,17 @@ export interface FeatureValue { [k: string]: unknown; } /** - * Additional metadata associated with the segment. + * Additional metadata associated with the feature. */ export interface Metadata { [k: string]: unknown; } +/** + * Additional metadata associated with the segment. + */ +export interface Metadata1 { + [k: string]: unknown; +} /** * Features to be evaluated in the context. */ diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts index 796db1b..7ff71b4 100644 --- a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -58,17 +58,24 @@ export interface FlagResult { enabled: Enabled; value: Value; reason: Reason; + metadata?: Metadata; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface Metadata { [k: string]: unknown; } export interface SegmentResult { key: Key; name: Name1; - metadata?: Metadata; + metadata?: Metadata1; [k: string]: unknown; } /** * Additional metadata associated with the segment. */ -export interface Metadata { +export interface Metadata1 { [k: string]: unknown; } diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index 7cf9df2..e2e1b22 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -5,16 +5,14 @@ import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; import { Flags } from '../../../sdk/models.js'; import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; import { parse as parseJsonc } from 'jsonc-parser'; +import { EvaluationResult } from '../../../flagsmith-engine/evaluation/models.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const TEST_DATA_DIR = path.join(__dirname, '../engine-tests/engine-test-data/test_cases'); interface TestCase { context: EvaluationContext; - result: { - flags?: Record; - segments?: Record; - }; + result: EvaluationResult; } function getTestFiles(): string[] { @@ -41,26 +39,8 @@ describe('Engine Integration Tests', () => { test(testName, () => { const testCase = loadTestFile(filePath); - 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 expectedFlags = testCase.result.flags || {}; - const sortedAPIFlags = Object.values(expectedFlags).sort((a: any, b: any) => - a.name > b.name ? 1 : -1 - ) as any[]; - - expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); - expect(engine_response.segments).toStrictEqual(testCase.result.segments); - expect(engine_response.flags).toStrictEqual(testCase.result.flags); - for (let i = 0; i < sortedEngineFlags.length; i++) { - expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i].value); - expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i].enabled); - expect(sortedEngineFlags[i].reason).toBe(sortedAPIFlags[i].reason); - } + expect(engine_response).toStrictEqual(testCase.result); }); }); }); From fe9db9e8160fd9970e4540733255309fac71311e Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Oct 2025 17:55:32 +0200 Subject: [PATCH 37/43] fix: fixed-type --- .gitmodules | 2 +- flagsmith-engine/evaluation/models.ts | 4 +++- flagsmith-engine/index.ts | 1 + tests/engine/engine-tests/engine-test-data | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0857b52..f9b9019 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 = v2.4.0 + branch = v2.5.0 diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index 334444b..f4fb564 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -18,7 +18,8 @@ import type { import type { EvaluationResult as EvaluationContextResult, - FlagResult as EvaluationContextResultFlagResult + FlagResult as EvaluationContextResultFlagResult, + Metadata } from './evaluationResult/evaluationResult.types.js'; export type EnvironmentKey = EnvironmentContext['key']; @@ -60,6 +61,7 @@ export type EvaluationResultFlags = Record< name: FeatureName; enabled: FeatureEnabled; value: FeatureValue; + metadata?: Metadata; reason: EvaluationReason; } >; diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 9eaa814..6954a8a 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -127,6 +127,7 @@ export function evaluateFeatures( name: finalFeature.name, enabled: finalFeature.enabled, value: evaluatedValue, + ...(finalFeature.metadata ? { metadata: { ...finalFeature.metadata } } : {}), reason: evaluatedReason ?? getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 6453b03..41c2021 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 6453b0391344a4d677a97cc4a9d27a8b8e329787 +Subproject commit 41c202145e375c712600e318c439456de5b221d7 From 778cdc4db10498342ec4d8d7e59c1447ab3e6bb2 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Oct 2025 18:27:32 +0200 Subject: [PATCH 38/43] feat: use-flagsmith-id-int-for-feature-id --- .../evaluation/evaluationContext/mappers.ts | 6 ++++-- flagsmith-engine/evaluation/models.ts | 19 ++++++++----------- flagsmith-engine/index.ts | 2 +- sdk/models.ts | 6 +++--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index c374aa0..7afa4bc 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -12,7 +12,6 @@ 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'; -import { parse as uuidParse } from 'uuid'; import { uuidToBigInt } from '../../features/util.js'; export function getEvaluationContext( @@ -57,7 +56,10 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): enabled: fs.enabled, value: fs.getValue(), variants, - priority: fs.featureSegment?.priority + priority: fs.featureSegment?.priority, + metadata: { + flagsmithId: fs.feature.id + } }; } diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index f4fb564..b165d3c 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -54,16 +54,15 @@ export type SegmentConditionOperator = SegmentCondition['operator']; export type EvaluationReason = EvaluationContextResultFlagResult['reason']; export type EvaluationResultSegments = EvaluationContextResult['segments']; -export type EvaluationResultFlags = Record< +import type { FlagResult } from './evaluationResult/evaluationResult.types.js'; + +export type FlagResultWithMetadata = FlagResult & { + metadata?: T; +}; + +export type EvaluationResultFlags = Record< string, - { - feature_key: FeatureKey; - name: FeatureName; - enabled: FeatureEnabled; - value: FeatureValue; - metadata?: Metadata; - reason: EvaluationReason; - } + FlagResultWithMetadata >; export type EvaluationResult = { @@ -71,8 +70,6 @@ export type EvaluationResult = { segments: EvaluationResultSegments; }; -export { FlagResult } from './evaluationResult/evaluationResult.types.js'; - export enum SegmentSource { API = 'api', IDENTITY_OVERRIDE = 'identity_override' diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 6954a8a..333dcad 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -127,7 +127,7 @@ export function evaluateFeatures( name: finalFeature.name, enabled: finalFeature.enabled, value: evaluatedValue, - ...(finalFeature.metadata ? { metadata: { ...finalFeature.metadata } } : {}), + ...(finalFeature.metadata ? { metadata: finalFeature.metadata } : {}), reason: evaluatedReason ?? getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) diff --git a/sdk/models.ts b/sdk/models.ts index e7ff613..f34f7b0 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,4 +1,4 @@ -import { EvaluationResult, FlagResult } from '../flagsmith-engine/evaluation/models.js'; +import { EvaluationResult, FlagResultWithMetadata } from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; @@ -81,11 +81,11 @@ export class Flag extends BaseFlag { }); } - static fromFlagResult(flagResult: FlagResult): Flag { + static fromFlagResult(flagResult: FlagResultWithMetadata<{ flagsmithId?: number }>): Flag { return new Flag({ enabled: flagResult.enabled, value: flagResult.value ?? null, - featureId: Number(flagResult.feature_key), + featureId: flagResult.metadata?.flagsmithId || Number(flagResult.feature_key), featureName: flagResult.name, reason: flagResult.reason }); From d8c23888e2b097bd465fe1a00f410a8a8113e34d Mon Sep 17 00:00:00 2001 From: wadii Date: Fri, 17 Oct 2025 11:50:33 +0200 Subject: [PATCH 39/43] feat: use-evaluation-context-generic-with-node-metadata-implementation --- .../evaluation/evaluationContext/mappers.ts | 4 +- flagsmith-engine/evaluation/models.ts | 82 ++++++++----------- flagsmith-engine/index.ts | 31 ++++--- sdk/models.ts | 10 ++- tests/engine/e2e/engine.test.ts | 9 +- 5 files changed, 73 insertions(+), 63 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 7afa4bc..1b97e7a 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -1,5 +1,5 @@ import { - Features, + FeaturesWithMetadata, Segments, Traits, EvaluationContext, @@ -38,7 +38,7 @@ function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): name: environment.project.name }; - const features: Features = {}; + const features: FeaturesWithMetadata = {}; for (const fs of environment.featureStates) { const variants = fs.multivariateFeatureStateValues?.length > 0 diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index b165d3c..17480e4 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -2,59 +2,44 @@ // 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, - SegmentContext, - SegmentRule, - SegmentCondition, - InSegmentCondition, - FeatureContext, - FeatureValue as ContextFeatureValue, - Traits, - Features, - Segments -} from './evaluationContext/evaluationContext.types.js'; - import type { EvaluationResult as EvaluationContextResult, - FlagResult as EvaluationContextResultFlagResult, + FlagResult, Metadata } from './evaluationResult/evaluationResult.types.js'; -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'] | InSegmentCondition['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']; +import type { + EvaluationContext as GeneratedEvaluationContext, + FeatureContext as GeneratedFeatureContext +} from './evaluationContext/evaluationContext.types.js'; -export type TraitMap = Traits; -export type FeatureMap = Features; -export type SegmentMap = Segments; +export interface FeatureMetadata extends Metadata { + flagsmithId: number; +} -export type SegmentConditionOperator = SegmentCondition['operator']; +export interface FeatureContext { + key: GeneratedFeatureContext['key']; + feature_key: GeneratedFeatureContext['feature_key']; + name: GeneratedFeatureContext['name']; + enabled: GeneratedFeatureContext['enabled']; + value: GeneratedFeatureContext['value']; + variants?: GeneratedFeatureContext['variants']; + priority?: GeneratedFeatureContext['priority']; + metadata?: T; + [k: string]: unknown; +} -export type EvaluationReason = EvaluationContextResultFlagResult['reason']; +export type FeaturesWithMetadata = { + [k: string]: FeatureContext; +}; -export type EvaluationResultSegments = EvaluationContextResult['segments']; -import type { FlagResult } from './evaluationResult/evaluationResult.types.js'; +export interface EvaluationContext { + environment: GeneratedEvaluationContext['environment']; + identity?: GeneratedEvaluationContext['identity']; + segments?: GeneratedEvaluationContext['segments']; + features?: FeaturesWithMetadata; + [k: string]: unknown; +} export type FlagResultWithMetadata = FlagResult & { metadata?: T; @@ -65,11 +50,16 @@ export type EvaluationResultFlags = Record< FlagResultWithMetadata >; -export type EvaluationResult = { - flags: EvaluationResultFlags; +export type EvaluationResultSegments = EvaluationContextResult['segments']; + +export type EvaluationResult = { + flags: EvaluationResultFlags; segments: EvaluationResultSegments; }; +export type EvaluationResultWithMetadata = EvaluationResult; +export type EvaluationContextWithMetadata = EvaluationContext; + export enum SegmentSource { API = 'api', IDENTITY_OVERRIDE = 'identity_override' diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index 333dcad..e9268ef 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,6 +1,12 @@ -import { EvaluationContext, FeatureContext, SegmentSource } from './evaluation/models.js'; +import { + EvaluationContextWithMetadata, + EvaluationResultSegments, + EvaluationResultWithMetadata, + FeatureContext, + FeatureMetadata +} from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { EvaluationResult, EvaluationResultFlags } from './evaluation/models.js'; +import { EvaluationResultFlags } from './evaluation/models.js'; import { TARGETING_REASONS } from './features/types.js'; import { getHashedPercentageForObjIds } from './utils/hashing/index.js'; export { EnvironmentModel } from './environments/models.js'; @@ -11,7 +17,7 @@ export { FeatureModel, FeatureStateModel } from './features/models.js'; export { OrganisationModel } from './organisations/models.js'; type SegmentOverride = { - feature: FeatureContext; + feature: FeatureContext; segmentName: string; }; @@ -27,7 +33,9 @@ export type SegmentOverrides = Record; * @param context - EvaluationContext containing environment, identity, and segment data * @returns EvaluationResult with flags, segments, and original context */ -export function getEvaluationResult(context: EvaluationContext): EvaluationResult { +export function getEvaluationResult( + context: EvaluationContextWithMetadata +): EvaluationResultWithMetadata { const { segments, segmentOverrides } = evaluateSegments(context); const flags = evaluateFeatures(context, segmentOverrides); @@ -40,12 +48,15 @@ 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 */ -export function evaluateSegments(context: EvaluationContext): { - segments: EvaluationResult['segments']; +export function evaluateSegments(context: EvaluationContextWithMetadata): { + segments: EvaluationResultSegments; segmentOverrides: Record; } { if (!context.identity || !context.segments) { - return { segments: [], segmentOverrides: {} }; + return { + segments: [], + segmentOverrides: {} as Record + }; } const identitySegments = getIdentitySegments(context); @@ -108,10 +119,10 @@ export function processSegmentOverrides(identitySegments: any[]): Record -): EvaluationResultFlags { - const flags: EvaluationResultFlags = {}; +): EvaluationResultFlags { + const flags: EvaluationResultFlags = {}; for (const feature of Object.values(context.features || {})) { const segmentOverride = segmentOverrides[feature.feature_key]; diff --git a/sdk/models.ts b/sdk/models.ts index f34f7b0..ac0c9cd 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,4 +1,8 @@ -import { EvaluationResult, FlagResultWithMetadata } from '../flagsmith-engine/evaluation/models.js'; +import { + EvaluationResult, + FeatureMetadata, + FlagResultWithMetadata +} from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; @@ -81,11 +85,11 @@ export class Flag extends BaseFlag { }); } - static fromFlagResult(flagResult: FlagResultWithMetadata<{ flagsmithId?: number }>): Flag { + static fromFlagResult(flagResult: FlagResultWithMetadata): Flag { return new Flag({ enabled: flagResult.enabled, value: flagResult.value ?? null, - featureId: flagResult.metadata?.flagsmithId || Number(flagResult.feature_key), + featureId: flagResult.metadata?.flagsmithId ?? Number(flagResult.feature_key), featureName: flagResult.name, reason: flagResult.reason }); diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index e2e1b22..a2a00b1 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -5,7 +5,10 @@ import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; import { Flags } from '../../../sdk/models.js'; import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; import { parse as parseJsonc } from 'jsonc-parser'; -import { EvaluationResult } from '../../../flagsmith-engine/evaluation/models.js'; +import { + EvaluationContextWithMetadata, + EvaluationResult +} from '../../../flagsmith-engine/evaluation/models.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -39,7 +42,9 @@ describe('Engine Integration Tests', () => { test(testName, () => { const testCase = loadTestFile(filePath); - const engine_response = getEvaluationResult(testCase.context); + const engine_response = getEvaluationResult( + testCase.context as EvaluationContextWithMetadata + ); expect(engine_response).toStrictEqual(testCase.result); }); }); From a475236fc12b6ba735e5b064c3f9d77234e6866e Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 20 Oct 2025 10:41:56 +0200 Subject: [PATCH 40/43] feat: use-latest-schemas-with-deduplicated-keys --- .../evaluationContext.types.ts | 16 +++---- .../evaluation/evaluationContext/mappers.ts | 8 ++-- .../evaluationResult.types.ts | 8 ++-- flagsmith-engine/evaluation/models.ts | 42 +++++++++---------- flagsmith-engine/index.ts | 14 +++---- flagsmith-engine/segments/evaluators.ts | 16 +++---- package.json | 4 +- sdk/models.ts | 4 +- 8 files changed, 55 insertions(+), 57 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index 6d04604..d1ec209 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -115,7 +115,7 @@ export type Weight = number; /** * Priority of the feature flag variant. Lower values indicate a higher priority when multiple variants apply to the same context key. */ -export type Priority = number; +export type VariantPriority = number; /** * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. */ @@ -123,7 +123,7 @@ 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 Priority1 = number; +export type FeaturePriority = number; /** * Feature overrides for the segment. */ @@ -179,7 +179,7 @@ export interface SegmentContext { name: Name1; rules: Rules; overrides?: Overrides; - metadata?: Metadata1; + metadata?: SegmentMetadata; [k: string]: unknown; } /** @@ -219,8 +219,8 @@ export interface FeatureContext { enabled: Enabled; value: Value2; variants?: Variants; - priority?: Priority1; - metadata?: Metadata; + priority?: FeaturePriority; + metadata?: FeatureMetadata; [k: string]: unknown; } /** @@ -229,19 +229,19 @@ export interface FeatureContext { export interface FeatureValue { value: Value3; weight: Weight; - priority: Priority; + priority: VariantPriority; [k: string]: unknown; } /** * Additional metadata associated with the feature. */ -export interface Metadata { +export interface FeatureMetadata { [k: string]: unknown; } /** * Additional metadata associated with the segment. */ -export interface Metadata1 { +export interface SegmentMetadata { [k: string]: unknown; } /** diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 1b97e7a..190cdd6 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -2,7 +2,7 @@ import { FeaturesWithMetadata, Segments, Traits, - EvaluationContext, + GenericEvaluationContext, EnvironmentContext, IdentityContext, SegmentSource @@ -18,7 +18,7 @@ export function getEvaluationContext( environment: EnvironmentModel, identity?: IdentityModel, overrideTraits?: TraitModel[] -): EvaluationContext { +): GenericEvaluationContext { const environmentContext = mapEnvironmentModelToEvaluationContext(environment); const identityContext = identity ? mapIdentityModelToIdentityContext(identity, overrideTraits) @@ -32,7 +32,9 @@ export function getEvaluationContext( return context; } -function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): EvaluationContext { +function mapEnvironmentModelToEvaluationContext( + environment: EnvironmentModel +): GenericEvaluationContext { const environmentContext: EnvironmentContext = { key: environment.apiKey, name: environment.project.name diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts index 7ff71b4..390146a 100644 --- a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -58,24 +58,24 @@ export interface FlagResult { enabled: Enabled; value: Value; reason: Reason; - metadata?: Metadata; + metadata?: FeatureMetadata; [k: string]: unknown; } /** * Additional metadata associated with the feature. */ -export interface Metadata { +export interface FeatureMetadata { [k: string]: unknown; } export interface SegmentResult { key: Key; name: Name1; - metadata?: Metadata1; + metadata?: SegmentMetadata; [k: string]: unknown; } /** * Additional metadata associated with the segment. */ -export interface Metadata1 { +export interface SegmentMetadata { [k: string]: unknown; } diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index 17480e4..1ff7228 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -5,60 +5,56 @@ import type { EvaluationResult as EvaluationContextResult, FlagResult, - Metadata + FeatureMetadata } from './evaluationResult/evaluationResult.types.js'; import type { - EvaluationContext as GeneratedEvaluationContext, - FeatureContext as GeneratedFeatureContext + FeatureContext, + EnvironmentContext, + IdentityContext, + SegmentContext } from './evaluationContext/evaluationContext.types.js'; -export interface FeatureMetadata extends Metadata { - flagsmithId: number; +export interface CustomFeatureMetadata extends FeatureMetadata { + flagsmithId?: number; } -export interface FeatureContext { - key: GeneratedFeatureContext['key']; - feature_key: GeneratedFeatureContext['feature_key']; - name: GeneratedFeatureContext['name']; - enabled: GeneratedFeatureContext['enabled']; - value: GeneratedFeatureContext['value']; - variants?: GeneratedFeatureContext['variants']; - priority?: GeneratedFeatureContext['priority']; +export interface FeatureContextWithMetadata + extends FeatureContext { metadata?: T; [k: string]: unknown; } -export type FeaturesWithMetadata = { - [k: string]: FeatureContext; +export type FeaturesWithMetadata = { + [k: string]: FeatureContextWithMetadata; }; -export interface EvaluationContext { - environment: GeneratedEvaluationContext['environment']; - identity?: GeneratedEvaluationContext['identity']; - segments?: GeneratedEvaluationContext['segments']; +export interface GenericEvaluationContext { + environment: EnvironmentContext; + identity?: IdentityContext; + segments?: SegmentContext; features?: FeaturesWithMetadata; [k: string]: unknown; } -export type FlagResultWithMetadata = FlagResult & { +export type FlagResultWithMetadata = FlagResult & { metadata?: T; }; -export type EvaluationResultFlags = Record< +export type EvaluationResultFlags = Record< string, FlagResultWithMetadata >; export type EvaluationResultSegments = EvaluationContextResult['segments']; -export type EvaluationResult = { +export type EvaluationResult = { flags: EvaluationResultFlags; segments: EvaluationResultSegments; }; export type EvaluationResultWithMetadata = EvaluationResult; -export type EvaluationContextWithMetadata = EvaluationContext; +export type EvaluationContextWithMetadata = GenericEvaluationContext; export enum SegmentSource { API = 'api', diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index e9268ef..d9ca878 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -2,8 +2,8 @@ import { EvaluationContextWithMetadata, EvaluationResultSegments, EvaluationResultWithMetadata, - FeatureContext, - FeatureMetadata + FeatureContextWithMetadata, + CustomFeatureMetadata } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; import { EvaluationResultFlags } from './evaluation/models.js'; @@ -17,7 +17,7 @@ export { FeatureModel, FeatureStateModel } from './features/models.js'; export { OrganisationModel } from './organisations/models.js'; type SegmentOverride = { - feature: FeatureContext; + feature: FeatureContextWithMetadata; segmentName: string; }; @@ -121,8 +121,8 @@ export function processSegmentOverrides(identitySegments: any[]): Record -): EvaluationResultFlags { - const flags: EvaluationResultFlags = {}; +): EvaluationResultFlags { + const flags: EvaluationResultFlags = {}; for (const feature of Object.values(context.features || {})) { const segmentOverride = segmentOverrides[feature.feature_key]; @@ -149,7 +149,7 @@ export function evaluateFeatures( } function evaluateFeatureValue( - feature: FeatureContext, + feature: FeatureContextWithMetadata, identityKey?: string ): { value: any; reason?: string } { if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { @@ -170,7 +170,7 @@ function evaluateFeatureValue( * @returns The variant value if the identity falls within a variant's range, otherwise the default feature value */ function getMultivariateFeatureValue( - feature: FeatureContext, + feature: FeatureContextWithMetadata, identityKey?: string ): { value: any; reason?: string } { const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index 44ebcf0..0b08526 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,6 +1,6 @@ import * as jsonpath from 'jsonpath'; import { - EvaluationContext, + GenericEvaluationContext, InSegmentCondition, SegmentCondition, SegmentContext, @@ -19,7 +19,7 @@ import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; * @param context - Evaluation context containing identity and segment definitions * @returns Array of segments that the identity matches */ -export function getIdentitySegments(context: EvaluationContext): SegmentContext[] { +export function getIdentitySegments(context: GenericEvaluationContext): SegmentContext[] { if (!context.identity || !context.segments) return []; return Object.values(context.segments).filter(segment => { @@ -45,7 +45,7 @@ export function getIdentitySegments(context: EvaluationContext): SegmentContext[ export function traitsMatchSegmentCondition( condition: SegmentCondition | InSegmentCondition, segmentKey: string, - context?: EvaluationContext + context?: GenericEvaluationContext ): boolean { if (condition.operator === PERCENTAGE_SPLIT) { const contextValueKey = @@ -81,7 +81,7 @@ export function traitsMatchSegmentCondition( function traitsMatchSegmentRule( rule: SegmentRule, segmentKey: string, - context?: EvaluationContext + context?: GenericEvaluationContext ): boolean { const matchesConditions = evaluateConditions(rule, segmentKey, context); const matchesSubRules = evaluateSubRules(rule, segmentKey, context); @@ -92,7 +92,7 @@ function traitsMatchSegmentRule( function evaluateConditions( rule: SegmentRule, segmentKey: string, - context?: EvaluationContext + context?: GenericEvaluationContext ): boolean { if (!rule.conditions || rule.conditions.length === 0) return true; @@ -106,7 +106,7 @@ function evaluateConditions( function evaluateSubRules( rule: SegmentRule, segmentKey: string, - context?: EvaluationContext + context?: GenericEvaluationContext ): boolean { if (!rule.rules || rule.rules.length === 0) return true; @@ -128,7 +128,7 @@ function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): } } -function getTraitValue(property: string, context?: EvaluationContext): any { +function getTraitValue(property: string, context?: GenericEvaluationContext): any { if (property.startsWith('$.')) { const contextValue = getContextValue(property, context); if (contextValue && !isNonPrimitive(contextValue)) { @@ -163,7 +163,7 @@ function isNonPrimitive(value: any): boolean { * @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 { +export function getContextValue(jsonPath: string, context?: GenericEvaluationContext): any { if (!context || !jsonPath?.startsWith('$.')) return undefined; try { diff --git a/package.json b/package.json index ede37a6..f244678 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/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-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/chore%2Fschema-niceties/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/chore%2Fschema-niceties/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": { diff --git a/sdk/models.ts b/sdk/models.ts index ac0c9cd..72147d2 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,6 +1,6 @@ import { EvaluationResult, - FeatureMetadata, + CustomFeatureMetadata, FlagResultWithMetadata } from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; @@ -85,7 +85,7 @@ export class Flag extends BaseFlag { }); } - static fromFlagResult(flagResult: FlagResultWithMetadata): Flag { + static fromFlagResult(flagResult: FlagResultWithMetadata): Flag { return new Flag({ enabled: flagResult.enabled, value: flagResult.value ?? null, From 2bbd1d11cf266e9b6e88f9578062580b4c1b2cd7 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 20 Oct 2025 14:09:47 +0200 Subject: [PATCH 41/43] feat: reverted-types-to-main-and-bumped-version --- flagsmith-engine/evaluation/models.ts | 10 +++++----- package.json | 6 +++--- sdk/models.ts | 15 ++++++++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index 1ff7228..757a147 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -12,7 +12,7 @@ import type { FeatureContext, EnvironmentContext, IdentityContext, - SegmentContext + Segments } from './evaluationContext/evaluationContext.types.js'; export interface CustomFeatureMetadata extends FeatureMetadata { @@ -31,8 +31,8 @@ export type FeaturesWithMetadata = export interface GenericEvaluationContext { environment: EnvironmentContext; - identity?: IdentityContext; - segments?: SegmentContext; + identity?: IdentityContext | null; + segments?: Segments; features?: FeaturesWithMetadata; [k: string]: unknown; } @@ -53,8 +53,8 @@ export type EvaluationResult = { segments: EvaluationResultSegments; }; -export type EvaluationResultWithMetadata = EvaluationResult; -export type EvaluationContextWithMetadata = GenericEvaluationContext; +export type EvaluationResultWithMetadata = EvaluationResult; +export type EvaluationContextWithMetadata = GenericEvaluationContext; export enum SegmentSource { API = 'api', diff --git a/package.json b/package.json index f244678..dff70e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith-nodejs", - "version": "6.1.0", + "version": "7.0.0", "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.", "main": "./build/cjs/index.js", "type": "module", @@ -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/chore%2Fschema-niceties/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/chore%2Fschema-niceties/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/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": { diff --git a/sdk/models.ts b/sdk/models.ts index 72147d2..0ee638c 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,7 @@ import { - EvaluationResult, CustomFeatureMetadata, - FlagResultWithMetadata + FlagResultWithMetadata, + EvaluationResultWithMetadata } from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; @@ -85,7 +85,7 @@ export class Flag extends BaseFlag { }); } - static fromFlagResult(flagResult: FlagResultWithMetadata): Flag { + static fromFlagResult(flagResult: FlagResultWithMetadata): Flag | null { return new Flag({ enabled: flagResult.enabled, value: flagResult.value ?? null, @@ -122,16 +122,21 @@ export class Flags { } static fromEvaluationResult( - evaluationResult: EvaluationResult, + evaluationResult: EvaluationResultWithMetadata, defaultFlagHandler?: (v: string) => DefaultFlag, analyticsProcessor?: AnalyticsProcessor ): Flags { const flags: { [key: string]: Flag } = {}; for (const flag of Object.values(evaluationResult.flags)) { + const flagsmithId = flag.metadata?.flagsmithId || Number(flag.feature_key); + if (!flagsmithId) { + continue; + } + flags[flag.name] = new Flag({ enabled: flag.enabled, value: flag.value ?? null, - featureId: Number(flag.feature_key), + featureId: flag.metadata?.flagsmithId || Number(flag.feature_key), featureName: flag.name, reason: flag.reason }); From 51dc67a7189794a1d38d012da5d3f78241732f82 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 21 Oct 2025 11:04:19 +0200 Subject: [PATCH 42/43] feat: re-added-fallback --- flagsmith-engine/evaluation/models.ts | 4 ++-- flagsmith-engine/index.ts | 5 +++-- sdk/models.ts | 12 +----------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts index 757a147..71ea56b 100644 --- a/flagsmith-engine/evaluation/models.ts +++ b/flagsmith-engine/evaluation/models.ts @@ -21,7 +21,7 @@ export interface CustomFeatureMetadata extends FeatureMetadata { export interface FeatureContextWithMetadata extends FeatureContext { - metadata?: T; + metadata: T; [k: string]: unknown; } @@ -38,7 +38,7 @@ export interface GenericEvaluationContext = FlagResult & { - metadata?: T; + metadata: T; }; export type EvaluationResultFlags = Record< diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index d9ca878..534b9e1 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -3,7 +3,8 @@ import { EvaluationResultSegments, EvaluationResultWithMetadata, FeatureContextWithMetadata, - CustomFeatureMetadata + CustomFeatureMetadata, + FlagResultWithMetadata } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; import { EvaluationResultFlags } from './evaluation/models.js'; @@ -142,7 +143,7 @@ export function evaluateFeatures( reason: evaluatedReason ?? getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) - }; + } as FlagResultWithMetadata; } return flags; diff --git a/sdk/models.ts b/sdk/models.ts index 0ee638c..bf4fc56 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -85,16 +85,6 @@ export class Flag extends BaseFlag { }); } - static fromFlagResult(flagResult: FlagResultWithMetadata): Flag | null { - return new Flag({ - enabled: flagResult.enabled, - value: flagResult.value ?? null, - featureId: flagResult.metadata?.flagsmithId ?? Number(flagResult.feature_key), - featureName: flagResult.name, - reason: flagResult.reason - }); - } - static fromAPIFlag(flagData: any): Flag { return new Flag({ enabled: flagData['enabled'], @@ -136,7 +126,7 @@ export class Flags { flags[flag.name] = new Flag({ enabled: flag.enabled, value: flag.value ?? null, - featureId: flag.metadata?.flagsmithId || Number(flag.feature_key), + featureId: flagsmithId, featureName: flag.name, reason: flag.reason }); From b95d127479aede25d7ee3e0f5a1c17d6df542491 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 21 Oct 2025 11:15:23 +0200 Subject: [PATCH 43/43] feat: added-metadata-to-segment-override-features-and-removed-fallback --- flagsmith-engine/evaluation/evaluationContext/mappers.ts | 5 ++++- sdk/models.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts index 190cdd6..dede412 100644 --- a/flagsmith-engine/evaluation/evaluationContext/mappers.ts +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -151,7 +151,10 @@ function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Seg name: fs.feature.name, enabled: fs.enabled, value: fs.getValue(), - priority: -Infinity + priority: -Infinity, + metadata: { + flagsmithId: fs.feature.id + } })); const overridesHash = createHash('sha1').update(JSON.stringify(overridesKey)).digest('hex'); diff --git a/sdk/models.ts b/sdk/models.ts index bf4fc56..f6c48a1 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -118,7 +118,7 @@ export class Flags { ): Flags { const flags: { [key: string]: Flag } = {}; for (const flag of Object.values(evaluationResult.flags)) { - const flagsmithId = flag.metadata?.flagsmithId || Number(flag.feature_key); + const flagsmithId = flag.metadata?.flagsmithId; if (!flagsmithId) { continue; }