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/.husky/pre-commit b/.husky/pre-commit index 938cbdb..c221482 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +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 test \ No newline at end of file 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/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts new file mode 100644 index 0000000..d105c96 --- /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. 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/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts new file mode 100644 index 0000000..a77b4de --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -0,0 +1,178 @@ +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'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../segments/constants.js'; +import { createHash } from 'node:crypto'; + +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 }) + }; + + 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 segmentOverrides: Segments = {}; + for (const segment of environment.project.segments) { + segmentOverrides[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 + })) + : [] + }; + } + + let identityOverrideSegments: Segments = {}; + if (environment.identityOverrides && environment.identityOverrides.length > 0) { + identityOverrideSegments = mapIdentityOverridesToSegments(environment.identityOverrides); + } + + return { + environment: environmentContext, + features, + segments: { + ...segmentOverrides, + ...identityOverrideSegments + } + }; +} + +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)) + }; +} + +function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Segments { + const segments: Segments = {}; + const featuresToIdentifiers = new Map(); + + for (const identity of identityOverrides) { + if (!identity.identityFeatures || identity.identityFeatures.length === 0) { + continue; + } + + 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/flagsmith-engine/evaluation/evaluationContext/types.ts b/flagsmith-engine/evaluation/evaluationContext/types.ts new file mode 100644 index 0000000..e671005 --- /dev/null +++ b/flagsmith-engine/evaluation/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/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts new file mode 100644 index 0000000..6644656 --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -0,0 +1,67 @@ +/* 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. + */ + +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled = boolean; +/** + * Feature flag value. + */ +export type Value = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * Unique segment identifier. + */ +export type Key = string; +/** + * Segment name. + */ +export type Name1 = string; +/** + * List of segments which the provided context belongs to. + */ +export type Segments = SegmentResult[]; + +/** + * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. + */ +export interface EvaluationResult { + flags: Flags; + segments: Segments; + [k: string]: unknown; +} +/** + * Feature flags evaluated for the context, mapped by feature names. + */ +export interface Flags { + [k: string]: FlagResult; +} +export interface FlagResult { + feature_key: FeatureKey; + name: Name; + enabled: Enabled; + value: Value; + reason: Reason; + [k: string]: unknown; +} +export interface SegmentResult { + key: Key; + name: Name1; + [k: string]: unknown; +} diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts new file mode 100644 index 0000000..ba9c511 --- /dev/null +++ b/flagsmith-engine/evaluation/models.ts @@ -0,0 +1,72 @@ +// 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, + SegmentContext, + SegmentRule, + SegmentCondition, + InSegmentCondition, + FeatureContext, + FeatureValue as ContextFeatureValue, + Traits, + Features, + 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']; + +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']; + +export type TraitMap = Traits; +export type FeatureMap = Features; +export type SegmentMap = Segments; + +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/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/types.ts b/flagsmith-engine/features/types.ts new file mode 100644 index 0000000..8f7fad1 --- /dev/null +++ b/flagsmith-engine/features/types.ts @@ -0,0 +1,4 @@ +export enum TARGETING_REASONS { + DEFAULT = 'DEFAULT', + TARGETING_MATCH = 'TARGETING_MATCH' +} diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index 0a19589..8136f0c 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 '../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); } diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index fb641ee..98a2434 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,102 +1,181 @@ -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 './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { SegmentModel } from './segments/models.js'; -import { FeatureStateNotFound } from './utils/errors.js'; - +import { EvaluationResult, 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'; -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; + +type SegmentOverride = { + feature: FeatureContext; + segmentName: string; +}; + +export type SegmentOverrides = Record; + +/** + * 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, segmentOverrides } = evaluateSegments(context); + const flags = evaluateFeatures(context, segmentOverrides); + + 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 + */ +export function evaluateSegments(context: EvaluationContext): { + segments: EvaluationResult['segments']; + segmentOverrides: Record; +} { + if (!context.identity || !context.segments) { + return { segments: [], segmentOverrides: {} }; } + const identitySegments = getIdentitySegments(context); - // 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; - } + 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 + */ +export function processSegmentOverrides(identitySegments: any[]): Record { + const segmentOverrides: Record = {}; + + 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 + }; } - featureStates[featureState.feature.id] = featureState; } } - // 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; + return segmentOverrides; } -export function getIdentityFeatureState( - environment: EnvironmentModel, - identity: IdentityModel, - featureName: string, - overrideTraits?: TraitModel[] -): FeatureStateModel { - const featureStates = getIdentityFeatureStatesDict(environment, identity, overrideTraits); +/** + * 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 + */ +export function evaluateFeatures( + context: EvaluationContext, + segmentOverrides: Record +): EvaluationResultFlags { + const flags: EvaluationResultFlags = []; - const matchingFeature = Object.values(featureStates).filter( - f => f.feature.name === featureName - ); + for (const feature of Object.values(context.features || {})) { + const segmentOverride = segmentOverrides[feature.feature_key]; + const finalFeature = segmentOverride ? segmentOverride.feature : feature; + const hasOverride = !!segmentOverride; + const reason = getTargetingMatchReason(segmentOverride); - if (matchingFeature.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); + flags.push({ + feature_key: finalFeature.feature_key, + name: finalFeature.name, + enabled: finalFeature.enabled, + value: hasOverride + ? finalFeature.value + : evaluateFeatureValue(finalFeature, context.identity?.key), + reason + }); } - return matchingFeature[0]; + return flags; } -export function getIdentityFeatureStates( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -): FeatureStateModel[] { - const featureStates = Object.values( - getIdentityFeatureStatesDict(environment, identity, overrideTraits) - ); - - if (environment.project.hideDisabledFlags) { - return featureStates.filter(fs => !!fs.enabled); +function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any { + if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { + return getMultivariateFeatureValue(feature, identityKey); } - return featureStates; + + return feature.value; } -export function getEnvironmentFeatureState(environment: EnvironmentModel, featureName: string) { - const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName); +/** + * 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]); - if (featuresStates.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); + 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; +} - return featuresStates[0]; +export function shouldApplyOverride( + override: any, + existingOverrides: Record +): boolean { + const currentOverride = existingOverrides[override.feature_key]; + return ( + !currentOverride || isHigherPriority(override.priority, currentOverride.feature.priority) + ); } -export function getEnvironmentFeatureStates(environment: EnvironmentModel): FeatureStateModel[] { - if (environment.project.hideDisabledFlags) { - return environment.featureStates.filter(fs => !!fs.enabled); - } - return environment.featureStates; +export function isHigherPriority( + priorityA: number | undefined, + priorityB: number | undefined +): boolean { + return (priorityA ?? Infinity) < (priorityB ?? Infinity); } + +const getTargetingMatchReason = (segmentOverride: SegmentOverride) => { + return segmentOverride + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}` + : 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 f5d0081..576b4d2 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,76 +1,163 @@ -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 * as jsonpath from 'jsonpath'; +import { + EvaluationContext, + InSegmentCondition, + SegmentCondition, + SegmentContext, + SegmentRule +} 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'; + +/** + * 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 => { + if (segment.rules.length === 0) return false; + return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); + }); } -export function evaluateIdentityInSegment( - identity: IdentityModel, - segment: SegmentModel, - overrideTraits?: TraitModel[] +/** + * 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 | InSegmentCondition, + segmentKey: string, + context?: EvaluationContext ): boolean { - return ( - segment.rules.length > 0 && - segment.rules.filter(rule => - traitsMatchSegmentRule( - overrideTraits || identity.identityTraits, - rule, - segment.id, - identity.djangoID || identity.compositeKey - ) - ).length === segment.rules.length - ); + if (condition.operator === PERCENTAGE_SPLIT) { + const contextValueKey = + getContextValue(condition.property, context) || context?.identity?.key; + const hashedPercentage = getHashedPercentageForObjIds([segmentKey, contextValueKey]); + return hashedPercentage <= parseFloat(String(condition.value)); + } + if (!condition.property) { + return false; + } + + const traitValue = getTraitValue(condition.property, context); + + if (condition.operator === IS_SET) { + return traitValue !== undefined && traitValue !== null; + } + if (condition.operator === IS_NOT_SET) { + return traitValue === undefined || traitValue === null; + } + + if (traitValue !== undefined && traitValue !== null) { + const segmentCondition = new SegmentConditionModel( + condition.operator, + condition.value as string, + condition.property + ); + return segmentCondition.matchesTraitValue(traitValue); + } + + return false; } function traitsMatchSegmentRule( - identityTraits: TraitModel[], - rule: SegmentRuleModel, - segmentId: number | string, - identityId: number | string + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext +): boolean { + const matchesConditions = evaluateConditions(rule, segmentKey, context); + const matchesSubRules = evaluateSubRules(rule, segmentKey, context); + + return matchesConditions && matchesSubRules; +} + +function evaluateConditions( + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext ): boolean { - const matchesConditions = - rule.conditions.length > 0 - ? rule.matchingFunction()( - rule.conditions.map(condition => - traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId) - ) - ) - : true; - return ( - matchesConditions && - rule.rules.filter(rule => - traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId) - ).length === rule.rules.length + 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); } -export function traitsMatchSegmentCondition( - identityTraits: TraitModel[], - condition: SegmentConditionModel, - segmentId: number | string, - identityId: number | string +function evaluateSubRules( + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext ): boolean { - if (condition.operator == PERCENTAGE_SPLIT) { - var hashedPercentage = getHashedPercentateForObjIds([segmentId, identityId]); - return hashedPercentage <= parseFloat(String(condition.value)); + 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': + 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 getTraitValue(property: string, context?: EvaluationContext): any { + if (property.startsWith('$.')) { + return getContextValue(property, context); + } + + const traits = context?.identity?.traits || {}; + 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; + + try { + const results = jsonpath.query(context, jsonPath); + return results.length > 0 ? results[0] : undefined; + } catch (error) { + 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..8858ff9 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,12 @@ import { CONDITION_OPERATORS } from './constants.js'; import { isSemver } from './util.js'; +import { + EvaluationContext, + Overrides +} from '../evaluation/evaluationContext/evaluationContext.types.js'; +import { CONSTANTS } from '../features/constants.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; @@ -56,17 +67,17 @@ export class SegmentConditionModel { }; operator: string; - value: string | null | undefined; - property_: 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; this.value = value; - this.property_ = property; + this.property = property; } matchesTraitValue(traitValue: any) { @@ -79,17 +90,49 @@ export class SegmentConditionModel { ); }, evaluateRegex: (traitValue: any) => { - return !!this.value && !!traitValue?.toString().match(new RegExp(this.value)); + 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.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: any) => { + 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()); } }; @@ -144,4 +187,70 @@ 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 = SegmentModel.createFeatureStatesFromOverrides( + segmentContext.overrides || [] + ); + segmentModels.push(segment); + } + } + + return segmentModels; + } + + private static createFeatureStatesFromOverrides(overrides: 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/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..330811c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,21 @@ "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", "@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 +46,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 +89,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 +663,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 +790,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 +1073,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 +1087,65 @@ "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/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", + "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 +1161,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 +1372,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 +1401,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 +1423,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 +1496,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 +1523,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", @@ -1320,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", @@ -1754,6 +2038,72 @@ "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/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", @@ -1764,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", @@ -1780,6 +2139,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", @@ -1790,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", @@ -1798,6 +2181,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 +2255,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 +2332,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 +2351,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 +2445,193 @@ "@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/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", + "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 +2675,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", @@ -2055,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", @@ -2116,6 +2848,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", @@ -2180,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", @@ -2195,6 +2948,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 +2994,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 +3129,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", @@ -2349,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", @@ -2366,6 +3174,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", @@ -2373,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", @@ -2468,50 +3298,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 +3320,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 +3376,31 @@ "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/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", @@ -2586,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", @@ -2838,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 aa632b6..ec45f87 100644 --- a/package.json +++ b/package.json @@ -57,20 +57,27 @@ "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/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": { + "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", "@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..b543541 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,22 +1,21 @@ import { Dispatcher } from 'undici-types'; -import { - getEnvironmentFeatureStates, - getIdentityFeatureStates -} 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'; -import { FlagsmithAPIError } from './errors.js'; +import { FlagsmithAPIError, FlagsmithClientError } 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 { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js'; +import { + SegmentModel, + EnvironmentModel, + IdentityModel, + TraitModel, + getEvaluationResult +} from '../flagsmith-engine/index.js'; import { Fetch, FlagsmithCache, @@ -25,6 +24,7 @@ import { TraitConfig } from './types.js'; import { pino, Logger } from 'pino'; +import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js'; export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js'; export { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; @@ -278,7 +278,13 @@ export class Flagsmith { })) ); - return getIdentitySegments(environment, identityModel); + 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); } private async fetchEnvironment(): Promise { @@ -397,14 +403,17 @@ 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); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } + const evaluationResult = getEvaluationResult(context); + const flags = Flags.fromEvaluationResult(evaluationResult); + if (!!this.cache) { await this.cache.set('flags', flags); } + return flags; } @@ -422,14 +431,17 @@ export class Flagsmith { })) ); - const featureStates = getIdentityFeatureStates(environment, identityModel); + const context = getEvaluationContext(environment, identityModel); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } + 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..2fdfbed 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,8 @@ +import { EvaluationResult, FlagResult } from '../flagsmith-engine/evaluation/models.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 +57,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 +76,15 @@ export class Flag extends BaseFlag { }); } + static fromFlagResult(flagResult: FlagResult): Flag { + return new Flag({ + enabled: flagResult.enabled, + value: flagResult.value ?? null, + featureId: Number(flagResult.feature_key), + featureName: flagResult.name + }); + } + static fromAPIFlag(flagData: any): Flag { return new Flag({ enabled: flagData['enabled'], @@ -99,6 +110,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..ff162a8 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -1,23 +1,16 @@ -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, - response: test_case['response'] + context: test_case['context'], + response: test_case['result'] }; }); return test_data; @@ -26,20 +19,21 @@ 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 sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) => - 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 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].getValue(testCase.identity.djangoID)).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 933f2ba..facf33a 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 933f2ba7aa6430797afc2d053530cfd005b461f6 +Subproject commit facf33a4c50fdabdce29899b19b9ea65ea70eb18 diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 15b27d1..1d6df05 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -1,8 +1,10 @@ import { - getEnvironmentFeatureState, - getEnvironmentFeatureStates, - getIdentityFeatureState, - getIdentityFeatureStates + 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'; @@ -11,102 +13,355 @@ import { environment, environmentWithSegmentOverride, feature1, - getEnvironmentFeatureStateForFeature, - getEnvironmentFeatureStateForFeatureByName, identity, identityInSegment, segmentConditionProperty, segmentConditionStringValue } from './utils.js'; +import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; +import { TARGETING_REASONS } from '../../../flagsmith-engine/features/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_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(TARGETING_REASONS.DEFAULT); }); -test('test_identity_get_feature_state_without_any_override_no_fs', () => { - expect(() => { - getIdentityFeatureState(environment(), identity(), '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)]; + env.identityOverrides = [ident]; + + const context = getEvaluationContext(env, ident); + const result = getEvaluationResult(context); - const featureStates = getIdentityFeatureStates(env, ident); + expect(result.flags.length).toBe(3); - expect(featureStates.length).toBe(3); - for (const featuresState of featureStates) { - const environmentFeatureState = getEnvironmentFeatureStateForFeature( - env, - featuresState.feature + for (const flag of result.flags) { + const environmentFeature = Object.values(context.features || {}).find( + f => f.name === flag.name + ); + + const expected = flag.name === 'overridden_feature' ? true : environmentFeature?.enabled; + + expect(flag.enabled).toBe(expected); + expect(flag.reason).toBe( + flag.name === 'overridden_feature' + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${IDENTITY_OVERRIDE_SEGMENT_NAME}` + : TARGETING_REASONS.DEFAULT ); - 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 featureStates = getIdentityFeatureStates( - environmentWithSegmentOverride(), - identityInSegment(), - [trait_models] + const context = getEvaluationContext(environmentWithSegmentOverride(), identityInSegment(), [ + trait_models + ]); + + const result = getEvaluationResult(context); + + const overriddenFlag = result.flags.find(f => f.value === 'segment_override'); + expect(overriddenFlag).toBeDefined(); + expect(overriddenFlag?.value).toBe('segment_override'); + expect(overriddenFlag?.reason).toEqual( + `${TARGETING_REASONS.TARGETING_MATCH}; segment=test name` ); - expect(featureStates[0].getValue()).toBe('segment_override'); }); -test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => { - const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); +test('test_environment_get_all_feature_states', () => { + const env = environment(); + const context = getEvaluationContext(env); + const result = getEvaluationResult(context); - const env = environmentWithSegmentOverride(); - env.project.hideDisabledFlags = true; + expect(result.flags.length).toBe(Object.keys(context.features || {}).length); - const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]); - expect(featureStates.length).toBe(0); + result.flags.forEach(flag => { + expect(flag.reason).toBe(TARGETING_REASONS.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); + } }); -test('test_environment_get_all_feature_states', () => { - const env = environment(); - const featureStates = getEnvironmentFeatureStates(env); +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(featureStates).toBe(env.featureStates); + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 2 }, existingOverrides)).toBe( + true + ); + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 10 }, existingOverrides)).toBe( + false + ); }); -test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => { - const env = environment(); +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' + } + } + }; - env.project.hideDisabledFlags = true; + const result = evaluateSegments(context); - const featureStates = getEnvironmentFeatureStates(env); + 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(featureStates).not.toBe(env.featureStates); - for (const fs of featureStates) { - expect(fs.enabled).toBe(true); - } + expect(Object.keys(result.segmentOverrides)).toEqual(['feature1']); + expect(result.segmentOverrides.feature1.segmentName).toBe('segment_with_overrides'); }); -test('test_environment_get_feature_state', () => { - const env = environment(); - const feature = feature1(); - const featureState = getEnvironmentFeatureState(env, feature.name); +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' + } + } + }; - expect(featureState.feature).toStrictEqual(feature); + 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('test_environment_get_feature_state_raises_feature_state_not_found', () => { - expect(() => { - getEnvironmentFeatureState(environment(), 'not_a_feature_name'); - }).toThrowError('Feature State Not Found'); +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'); }); diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 1a73eec..6d260c7 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -3,19 +3,27 @@ import { CONDITION_OPERATORS, PERCENTAGE_SPLIT } from '../../../../flagsmith-engine/segments/constants.js'; -import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js'; + import { traitsMatchSegmentCondition, - 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 { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getEvaluationContext } from '../../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; +import { + EvaluationContext, + InSegmentCondition, + SegmentCondition, + SegmentCondition1 +} 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', () => ({ - getHashedPercentateForObjIds: vi.fn(() => 1) + getHashedPercentageForObjIds: vi.fn(() => 1) })); let traitExistenceTestCases: [ @@ -48,14 +56,33 @@ 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( - expectedResult - ); + let segmentConditionModel = { + operator, + value: conditionValue, + property: 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 as SegmentCondition, 'any', context) + ).toBe(expectedResult); } }); -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(), [], @@ -84,13 +111,376 @@ 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); + var result = getIdentitySegments(context); - expect(result).toBe(true); - expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1); - expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([ - segmentModel.id, - identityModel.djangoID + expect(result).toHaveLength(1); + expect(getHashedPercentageForObjIds).toHaveBeenCalledTimes(1); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith([ + result[0].key, + 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('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('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 empty array for segment with no rules', () => { + const context = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'empty_segment', + rules: [], + overrides: [] + } + } + }; + + expect(getIdentitySegments(context)).toEqual([]); + }); + + test('returns segment when all rules match', () => { + const context: EvaluationContext = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'matching_segment', + rules: [ + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ], + rules: [] + }, + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'CONTAINS', + value: 'test@example.com' + } + ], + rules: [] + } + ], + overrides: [] + } + } + }; + + const result = getIdentitySegments(context); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('matching_segment'); + }); + + 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: [] + }, + { + type: ALL_RULE, + conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }], + rules: [] + } + ], + overrides: [] + } + } + }; + + expect(getIdentitySegments(context)).toEqual([]); + }); +}); + +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.toString() + } as SegmentCondition1 | InSegmentCondition; + const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext); + + expect(result).toBe(expected); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith(['seg1', 'user-123']); + }); +}); 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); +}); 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) {