Compare commits
No commits in common. "main" and "nestjs-sdk-v0.1.1-experimental" have entirely different histories.
main
...
nestjs-sdk
115
.eslintrc.json
115
.eslintrc.json
|
@ -1,54 +1,63 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignorePatterns": ["**/dist/**/*"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:jsdoc/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "check-file", "jsdoc"],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"disallowTypeAnnotations": true,
|
||||
"fixStyle": "separate-type-imports",
|
||||
"prefer": "type-imports"
|
||||
}
|
||||
],
|
||||
"jsdoc/require-jsdoc": [
|
||||
"warn",
|
||||
{
|
||||
"publicOnly": true
|
||||
}
|
||||
],
|
||||
"jsdoc/check-tag-names": [
|
||||
"warn",
|
||||
{
|
||||
"definedTags": ["experimental"]
|
||||
}
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": ["error", "always"],
|
||||
"check-file/filename-naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"**/*.{js,ts}": "KEBAB_CASE"
|
||||
},
|
||||
{
|
||||
"ignoreMiddleExtensions": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignorePatterns": ["**/dist/**/*"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"plugin:jsdoc/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"check-file",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"jsdoc/require-jsdoc": [
|
||||
"warn",
|
||||
{
|
||||
"publicOnly": true
|
||||
}
|
||||
],
|
||||
"jsdoc/check-tag-names": [
|
||||
"warn",
|
||||
{
|
||||
"definedTags": [
|
||||
"experimental"
|
||||
]
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"check-file/filename-naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"**/*.{js,ts}": "KEBAB_CASE"
|
||||
},
|
||||
{
|
||||
"ignoreMiddleExtensions": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -29,6 +29,21 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: 'npm'
|
||||
|
||||
# if this is an @openfeature/core release, but the SDKs to use this version as a peer, and commit back
|
||||
- name: Update Peer Version in Dependants
|
||||
if: ${{ endsWith(github.ref_name, env.CORE_PACKAGE) }}
|
||||
run: |
|
||||
npm run update-core-peers && \
|
||||
! git diff-files --quiet && \
|
||||
( echo 'Updated peer dependency in dependents, committing...'
|
||||
git add --all && \
|
||||
git config user.name "openfeature-peer-update-bot" && \
|
||||
git config user.email "openfeature-peer-update-bot@openfeature.dev" && \
|
||||
git commit -m 'fix: bump @openfeature/${{ env.CORE_PACKAGE }} peer' -s && \
|
||||
git push ) || echo 'Peer dependency in dependents is already up to date.'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install
|
||||
|
|
|
@ -16,9 +16,9 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 16.x
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 22.x
|
||||
- 24.x
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -37,11 +37,8 @@ jobs:
|
|||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Test Jest Projects
|
||||
run: npm run test:jest
|
||||
|
||||
- name: Test Angular SDK
|
||||
run: npm run test:angular
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
codecov-and-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -50,7 +47,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install
|
||||
|
@ -72,7 +69,8 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# we need 'fetch' for this test, which is only in 18
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install
|
||||
|
@ -82,4 +80,4 @@ jobs:
|
|||
run: npm run build
|
||||
|
||||
- name: SDK e2e tests
|
||||
run: npm run e2e
|
||||
run: npm run e2e
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
npm install -g npm@^10.2.0
|
||||
|
@ -54,10 +54,6 @@ jobs:
|
|||
needs: release-please
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
environment: publish
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
# The logic below handles the npm publication:
|
||||
- name: Checkout Repository
|
||||
|
@ -65,7 +61,7 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: 'npm'
|
||||
- name: Build Packages
|
||||
|
@ -78,8 +74,6 @@ jobs:
|
|||
- name: Publish to NPM
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
# https://docs.npmjs.com/generating-provenance-statements
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
run: npm run publish-all
|
||||
|
||||
- name: Build Docs
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"packages/nest": "0.2.5",
|
||||
"packages/react": "1.0.1",
|
||||
"packages/web": "1.6.1",
|
||||
"packages/server": "1.19.0",
|
||||
"packages/shared": "1.9.0",
|
||||
"packages/angular/projects/angular-sdk": "0.0.16"
|
||||
"packages/nest": "0.1.1-experimental",
|
||||
"packages/react": "0.2.0-experimental",
|
||||
"packages/client": "0.4.14",
|
||||
"packages/server": "1.13.0",
|
||||
"packages/shared": "0.0.27"
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"domainless"
|
||||
]
|
||||
}
|
|
@ -3,4 +3,4 @@
|
|||
#
|
||||
# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-javascript/workgroup.yaml
|
||||
#
|
||||
* @open-feature/sdk-javascript-maintainers @open-feature/maintainers
|
||||
* @open-feature/sdk-javascript-maintainers
|
||||
|
|
|
@ -8,7 +8,7 @@ node 16+, npm 8+ are recommended.
|
|||
|
||||
### Compilation Target(s)
|
||||
|
||||
We target `es2015`, and publish both ES-modules and CommonJS modules.
|
||||
We target `es2022`, and publish both ES-modules and CommonJS modules.
|
||||
|
||||
### Installation and Dependencies
|
||||
|
||||
|
@ -19,7 +19,7 @@ We value having as few runtime dependencies as possible. The addition of any dep
|
|||
### Modules
|
||||
|
||||
This repository uses [NPM workspaces](https://docs.npmjs.com/cli/v9/using-npm/workspaces) to establish a simple monorepo.
|
||||
Within the root project, there is one common project (`packages/shared`) which features common interfaces and code, consumed by the published modules (`packages/server` and `packages/web`).
|
||||
Within the root project, there is one common project (`packages/shared`) which features common interfaces and code, consumed by the published modules (`packages/server` and `packages/client`).
|
||||
The shared module is built and published separately, and is a peer dependency of the SDK packages.
|
||||
Consumers need not install it separately, since `npm` and `yarn` automatically install required peers.
|
||||
In order to prevent regressions cause by incompatibilities due to version mismatches, the SDKs are locked to a particular version of the `@openfeature/core` module, and the CI enforces that it's released before any dependant SDKs (see [the related workflow](./.github/workflows/audit-pending-releases.yml)).
|
||||
|
@ -36,9 +36,9 @@ npm run e2e-server
|
|||
```
|
||||
for the server e2e tests and
|
||||
```
|
||||
npm run e2e-web
|
||||
npm run e2e-client
|
||||
```
|
||||
for the web e2e tests.
|
||||
for the client e2e tests.
|
||||
|
||||
### Packaging
|
||||
|
||||
|
@ -120,16 +120,6 @@ on each other), the owner should try to get people aligned by:
|
|||
- If none of the above worked and the PR has been stuck for more than 2 weeks,
|
||||
the owner should bring it to the OpenFeatures [meeting](README.md#contributing).
|
||||
|
||||
## Releasing
|
||||
|
||||
As with most OpenFeature repos, release-please supports our release process.
|
||||
For this SDK specifically, keep in mind this is a monorepo with dependencies with between components.
|
||||
If there are multiple release PRs open, ensure that you release them in order consistent with their dependency graph, waiting for each to fully complete.
|
||||
For example, if there are pending releases for: `@openfeature/core`, `@openfeature/web-sdk` and `@openfeature/react-sdk`, release them in that order.
|
||||
|
||||
Also ensure that if there are changes in an artifact which depend on changes in a dependency, that you reflect that in the `peerDependencies` field.
|
||||
For example, if a new release of `@openfeature/web-sdk` depends on features added in `@openfeature/core`, update the required minimum version of the `@openfeature/core` peer in the `@openfeature/web-sdk` package.json.
|
||||
|
||||
## Design Choices
|
||||
|
||||
As with other OpenFeature SDKs, js-sdk follows the
|
||||
|
|
|
@ -29,9 +29,7 @@ This repository contains both the server-side JS and web-browser SDKs.
|
|||
For details, including API documentation, see the respective README files.
|
||||
|
||||
- [Server SDK](./packages/server/README.md), for use in Node.js and similar runtimes.
|
||||
- [NestJS SDK](./packages/nest/README.md), a distribution of the Server SDK with built-in NestJS-specific features.
|
||||
- [Web SDK](./packages/web/README.md), for use in the web browser.
|
||||
- [React SDK](./packages/react//README.md), a distribution of the Web SDK with built-in React-specific features.
|
||||
- [Client SDK](./packages/client/README.md), for use in the web browser.
|
||||
|
||||
Each have slightly different APIs, but share many underlying types and components.
|
||||
|
||||
|
|
|
@ -124,10 +124,10 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
displayName: 'web',
|
||||
displayName: 'client',
|
||||
testEnvironment: 'node',
|
||||
preset: 'ts-jest',
|
||||
testMatch: ['<rootDir>/packages/web/test/**/*.spec.ts'],
|
||||
testMatch: ['<rootDir>/packages/client/test/**/*.spec.ts'],
|
||||
moduleNameMapper: {
|
||||
'@openfeature/core': '<rootDir>/packages/shared/src',
|
||||
},
|
||||
|
@ -143,10 +143,10 @@ export default {
|
|||
},
|
||||
},
|
||||
{
|
||||
displayName: 'web-e2e',
|
||||
displayName: 'client-e2e',
|
||||
testEnvironment: 'node',
|
||||
preset: 'ts-jest',
|
||||
testMatch: ['<rootDir>/packages/web/e2e/**/*.spec.ts'],
|
||||
testMatch: ['<rootDir>/packages/client/e2e/**/*.spec.ts'],
|
||||
modulePathIgnorePatterns: ['.*/node-modules/'],
|
||||
moduleNameMapper: {
|
||||
'^uuid$': require.resolve('uuid'),
|
||||
|
@ -161,7 +161,6 @@ export default {
|
|||
testMatch: ['<rootDir>/packages/nest/test/**/*.spec.ts'],
|
||||
moduleNameMapper: {
|
||||
'@openfeature/core': '<rootDir>/packages/shared/src',
|
||||
'@openfeature/server-sdk': '<rootDir>/packages/server/src',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
|
@ -172,24 +171,6 @@ export default {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'react',
|
||||
testEnvironment: 'jsdom',
|
||||
preset: 'ts-jest',
|
||||
testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
|
||||
moduleNameMapper: {
|
||||
'@openfeature/core': '<rootDir>/packages/shared/src',
|
||||
'@openfeature/web-sdk': '<rootDir>/packages/web/src',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
|
|
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
|
@ -1,24 +1,20 @@
|
|||
{
|
||||
"name": "@openfeature/js",
|
||||
"engines": {
|
||||
"npm": "^10.0.0"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "OpenFeature SDK for JavaScript",
|
||||
"scripts": {
|
||||
"test": "npm run test:jest && npm run test:angular",
|
||||
"test:jest": "jest --selectProjects=shared --selectProjects=server --selectProjects=web --selectProjects=react --selectProjects=nest --silent",
|
||||
"test:angular": "npm run test:coverage --workspace=packages/angular",
|
||||
"test": "jest --selectProjects=shared --selectProjects=server --selectProjects=client --silent",
|
||||
"e2e-server": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/server/e2e/features && jest --selectProjects=server-e2e --verbose",
|
||||
"e2e-web": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/web/e2e/features && jest --selectProjects=web-e2e --verbose",
|
||||
"e2e": "npm run e2e-server && npm run e2e-web",
|
||||
"lint": "npm run lint --workspace=packages/shared --workspace=packages/server --workspace=packages/web --workspace=packages/react --workspace=packages/angular --workspace=packages/nest",
|
||||
"lint:fix": "npm run lint:fix --workspace=packages/shared --workspace=packages/server --workspace=packages/web --workspace=packages/react --workspace=packages/angular --workspace=packages/nest",
|
||||
"e2e-client": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/client/e2e/features && jest --selectProjects=client-e2e --verbose",
|
||||
"e2e": "npm run e2e-server && npm run e2e-client",
|
||||
"lint": "npm run lint --workspace=packages/shared --workspace=packages/server --workspace=packages/client --workspace=packages/react --workspace=packages/nest",
|
||||
"clean": "shx rm -rf ./dist",
|
||||
"build": "npm run build --workspace=packages/shared --workspace=packages/server --workspace=packages/web --workspace=packages/react --workspace=packages/angular --workspace=packages/nest",
|
||||
"publish-all": "npm run publish-if-not-exists --workspace=packages/shared --workspace=packages/server --workspace=packages/web --workspace=packages/react --workspace=packages/angular --workspace=packages/nest",
|
||||
"docs": "typedoc"
|
||||
"build": "npm run build --workspace=packages/shared --workspace=packages/server --workspace=packages/client --workspace=packages/react --workspace=packages/nest",
|
||||
"publish-all": "npm run publish-if-not-exists --workspace=packages/shared --workspace=packages/server --workspace=packages/client --workspace=packages/react --workspace=packages/nest",
|
||||
"docs": "typedoc",
|
||||
"core-version": "npm run version --workspace=packages/shared",
|
||||
"update-core-peers": "export OPENFEATURE_CORE_VERSION=$(npm run --silent core-version) && npm run update-core-peer --workspace=packages/server --workspace=packages/client"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -36,49 +32,48 @@
|
|||
"url": "https://github.com/open-feature/js-sdk/issues"
|
||||
},
|
||||
"homepage": "https://github.com/open-feature/js-sdk#readme",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^12.0.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/events": "^3.0.3",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-check-file": "^2.6.2",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"eslint-plugin-jest": "^27.6.3",
|
||||
"eslint-plugin-jsdoc": "^48.0.6",
|
||||
"events": "^3.3.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-config": "^29.7.0",
|
||||
"jest-cucumber": "^4.0.0",
|
||||
"jest-cucumber": "^3.0.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^18.2.0",
|
||||
"rollup": "^4.0.0",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"shx": "^0.4.0",
|
||||
"rollup": "^3.29.4",
|
||||
"rollup-plugin-dts": "^5.3.1",
|
||||
"shx": "^0.3.4",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.3.0",
|
||||
"typedoc": "^0.26.0",
|
||||
"typedoc": "^0.25.7",
|
||||
"typescript": "^4.7.4",
|
||||
"uuid": "^11.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/shared",
|
||||
"packages/server",
|
||||
"packages/web",
|
||||
"packages/client",
|
||||
"packages/react",
|
||||
"packages/angular",
|
||||
"packages/angular/projects/angular-sdk",
|
||||
"packages/nest"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended",
|
||||
"plugin:@angular-eslint/template/accessibility"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
|
@ -1,27 +0,0 @@
|
|||
# Angular
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.2.2.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"$schema": "../../node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-sdk": {
|
||||
"projectType": "library",
|
||||
"root": "projects/angular-sdk",
|
||||
"sourceRoot": "projects/angular-sdk/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/angular-sdk/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/angular-sdk/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/angular-sdk/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"projects/angular-sdk/**/*.ts",
|
||||
"projects/angular-sdk/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"tsConfig": "projects/angular-sdk/tsconfig.spec.json",
|
||||
"providersFile": "projects/angular-sdk/src/test-provider.ts",
|
||||
"runner": "vitest",
|
||||
"buildTarget": "::development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
],
|
||||
"analytics": false
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"name": "angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"lint": "ng lint",
|
||||
"lint:fix": "ng lint --fix",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"test:coverage": "ng test --no-watch --code-coverage",
|
||||
"build": "ng build && npm run postbuild",
|
||||
"postbuild": "shx cp ./../../LICENSE ./dist/angular/LICENSE",
|
||||
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm --prefix dist/angular run current-published-version -s)\" = \"$(npm --prefix dist/angular run current-version -s)\" ]; then echo 'already published, skipping'; else cd dist/angular && npm publish --access public; fi"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "^20.1.1",
|
||||
"@angular-eslint/eslint-plugin": "^20.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^20.1.1",
|
||||
"@angular-eslint/schematics": "^20.1.1",
|
||||
"@angular-eslint/template-parser": "^20.1.1",
|
||||
"@angular/animations": "^20.1.1",
|
||||
"@angular/build": "^20.1.1",
|
||||
"@angular/cli": "^20.1.1",
|
||||
"@angular/common": "^20.1.1",
|
||||
"@angular/compiler": "^20.1.1",
|
||||
"@angular/compiler-cli": "^20.1.1",
|
||||
"@angular/core": "^20.1.1",
|
||||
"@angular/forms": "^20.1.1",
|
||||
"@angular/platform-browser": "^20.1.1",
|
||||
"@angular/platform-browser-dynamic": "^20.1.1",
|
||||
"@angular/router": "^20.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^8.57.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"ng-packagr": "^20.1.0",
|
||||
"playwright": "^1.53.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4",
|
||||
"zone.js": "~0.15.0"
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For Angular's browser support policy, please see:
|
||||
# https://angular.dev/reference/versions#browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
Chrome >= 107
|
||||
ChromeAndroid >= 107
|
||||
Edge >= 107
|
||||
Firefox >= 104
|
||||
FirefoxAndroid >= 104
|
||||
Safari >= 16
|
||||
iOS >= 16
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"extends": "../../.eslintrc.json",
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [0.0.16](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.15...angular-sdk-v0.0.16) (2025-07-25)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* support Angular 20 ([#1220](https://github.com/open-feature/js-sdk/issues/1220)) ([aa232a9](https://github.com/open-feature/js-sdk/commit/aa232a9d6a8dfa416380ccdecd71843d3e361048))
|
||||
|
||||
|
||||
## [0.0.15](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.14...angular-sdk-v0.0.15) (2025-05-27)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **angular:** update docs ([#1200](https://github.com/open-feature/js-sdk/issues/1200)) ([b6ea588](https://github.com/open-feature/js-sdk/commit/b6ea5884f2ab9f4f94c8b258c4cf7268ea6dbeb8))
|
||||
|
||||
|
||||
## [0.0.14](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.13...angular-sdk-v0.0.14) (2025-05-25)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **angular:** add license and url field to package.json ([b2784f5](https://github.com/open-feature/js-sdk/commit/b2784f53b85a11c58abb8e2a0f87a31890885c54))
|
||||
|
||||
|
||||
## [0.0.13](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.12...angular-sdk-v0.0.13) (2025-04-20)
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* fix readme typo ([#1174](https://github.com/open-feature/js-sdk/issues/1174)) ([21a32ec](https://github.com/open-feature/js-sdk/commit/21a32ec92ecde9ec43c9d72b5921035af13448d1))
|
||||
|
||||
## [0.0.12](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.11...angular-sdk-v0.0.12) (2025-04-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **angular:** add docs for setting evaluation context in angular ([#1170](https://github.com/open-feature/js-sdk/issues/1170)) ([24f1b23](https://github.com/open-feature/js-sdk/commit/24f1b230bf1d57971a336ac21b9ee46e8baf0cab))
|
||||
|
||||
|
||||
## [0.0.11](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.10...angular-sdk-v0.0.11) (2025-04-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **angular:** add option for initial context injection ([aafdb43](https://github.com/open-feature/js-sdk/commit/aafdb4382f113f96a649f5fc0cecadb4178ada67))
|
||||
|
||||
|
||||
## [0.0.10](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.9-experimental...angular-sdk-v0.0.10) (2025-02-13)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **angular:** update angular package to a non-experimental version ([#1147](https://github.com/open-feature/js-sdk/issues/1147)) ([5272f76](https://github.com/open-feature/js-sdk/commit/5272f76c4075ebbd21f9b24dacac8f2d22e31ca9)), closes [#1110](https://github.com/open-feature/js-sdk/issues/1110)
|
||||
* update sdk peer ([#1142](https://github.com/open-feature/js-sdk/issues/1142)) ([8bb6206](https://github.com/open-feature/js-sdk/commit/8bb620601e2b8dc7b62d717169b585bd1c886996))
|
||||
|
||||
## [0.0.9-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.8-experimental...angular-sdk-v0.0.9-experimental) (2024-11-21)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **angular:** add repository to package.json ([#1093](https://github.com/open-feature/js-sdk/issues/1093)) ([35f000e](https://github.com/open-feature/js-sdk/commit/35f000e0f3c3ff7d60c05883312691d14f01c5fd))
|
||||
|
||||
## [0.0.8-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.7-experimental...angular-sdk-v0.0.8-experimental) (2024-11-21)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **angular:** add angular 19 to peerDependencies ([4893d6f](https://github.com/open-feature/js-sdk/commit/4893d6f0003fbdcdcd4c7c061e9aed49e20b8976))
|
||||
|
||||
|
||||
## [0.0.7-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.6-experimental...angular-sdk-v0.0.7-experimental) (2024-11-21)
|
||||
|
||||
|
||||
Note: This version did not release
|
||||
|
||||
|
||||
## [0.0.6-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.5-experimental...angular-sdk-v0.0.6-experimental) (2024-10-28)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **angular:** add Angular 18 support ([#1063](https://github.com/open-feature/js-sdk/issues/1063)) ([e62d6d4](https://github.com/open-feature/js-sdk/commit/e62d6d4b7e4a5d0f40592a2c73e7124d22eec98e))
|
||||
|
||||
|
||||
## [0.0.5-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.4-experimental...angular-sdk-v0.0.5-experimental) (2024-10-21)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **angular:** fix race condition on initialization ([#1052](https://github.com/open-feature/js-sdk/issues/1052)) ([12eaa97](https://github.com/open-feature/js-sdk/commit/12eaa9758d9deb788d74488ef03f18cbd31c0cbe))
|
||||
|
||||
|
||||
## [0.0.4-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.3-experimental...angular-sdk-v0.0.4-experimental) (2024-09-30)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **angular:** add package description ([#1026](https://github.com/open-feature/js-sdk/issues/1026)) ([dc63ca8](https://github.com/open-feature/js-sdk/commit/dc63ca8b9d6fe8c16089e95f0e336d5e3f759f3b))
|
||||
|
||||
## [0.0.3-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.2-experimental...angular-sdk-v0.0.3-experimental) (2024-09-22)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* add npm keywords for angular ([#1015](https://github.com/open-feature/js-sdk/issues/1015)) ([6b11165](https://github.com/open-feature/js-sdk/commit/6b11165aa102e62fb8cd4dd218643e2ef0e733cf))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* **angular:** improve angular readme layout ([#1013](https://github.com/open-feature/js-sdk/issues/1013)) ([ee52da9](https://github.com/open-feature/js-sdk/commit/ee52da9a01fe71fd5b4a4734659a06c48b6dc62c))
|
||||
|
||||
## [0.0.2-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.1-experimental...angular-sdk-v0.0.2-experimental) (2024-09-14)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* copy license to package correctly ([#1011](https://github.com/open-feature/js-sdk/issues/1011)) ([458d278](https://github.com/open-feature/js-sdk/commit/458d278345fe8681a966fca3852b2e607bdafccb))
|
||||
|
||||
## [0.0.1-experimental](https://github.com/open-feature/js-sdk/compare/angular-sdk-v0.0.2-experimental...angular-sdk-v0.0.3-experimental) (2024-09-14)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* Angular SDK ([#997](https://github.com/open-feature/js-sdk/issues/997)) ([105fd95](https://github.com/open-feature/js-sdk/commit/105fd95e344822ffcfc54d328a28676b6f27f38e))
|
|
@ -1,352 +0,0 @@
|
|||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- x-hide-in-docs-start -->
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg" />
|
||||
<img align="center" alt="OpenFeature Logo" src="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<h2 align="center">OpenFeature Angular SDK</h2>
|
||||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-start-version -->
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/angular-sdk-v0.0.16">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.0.16&color=blue&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-end -->
|
||||
<br/>
|
||||
<a href="https://codecov.io/gh/open-feature/js-sdk">
|
||||
<img alt="codecov" src="https://codecov.io/gh/open-feature/js-sdk/branch/main/graph/badge.svg?token=3DC5XOEHMY" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/@openfeature/angular-sdk">
|
||||
<img alt="NPM Download" src="https://img.shields.io/npm/dm/%40openfeature%2Fangular-sdk" />
|
||||
</a>
|
||||
</p>
|
||||
<!-- x-hide-in-docs-start -->
|
||||
|
||||
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API
|
||||
for feature flagging that works with your favorite feature flag management tool or in-house solution.
|
||||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
|
||||
## Overview
|
||||
|
||||
The OpenFeature Angular SDK adds Angular-specific functionality to
|
||||
the [OpenFeature Web SDK](https://openfeature.dev/docs/reference/technologies/client/web).
|
||||
|
||||
In addition to the features provided by the [web sdk](https://openfeature.dev/docs/reference/technologies/client/web), capabilities include:
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick start](#quick-start)
|
||||
- [Requirements](#requirements)
|
||||
- [Install](#install)
|
||||
- [npm](#npm)
|
||||
- [yarn](#yarn)
|
||||
- [Required peer dependencies](#required-peer-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [Module](#module)
|
||||
- [Minimal Example](#minimal-example)
|
||||
- [How to use](#how-to-use)
|
||||
- [Boolean Feature Flag](#boolean-feature-flag)
|
||||
- [Number Feature Flag](#number-feature-flag)
|
||||
- [String Feature Flag](#string-feature-flag)
|
||||
- [Object Feature Flag](#object-feature-flag)
|
||||
- [Opting-out of automatic re-rendering](#opting-out-of-automatic-re-rendering)
|
||||
- [Consuming the evaluation details](#consuming-the-evaluation-details)
|
||||
- [Setting Evaluation Context](#setting-evaluation-context)
|
||||
- [FAQ and troubleshooting](#faq-and-troubleshooting)
|
||||
- [Resources](#resources)
|
||||
|
||||
## Quick start
|
||||
|
||||
### Requirements
|
||||
|
||||
- ES2015-compatible web browser (Chrome, Edge, Firefox, etc)
|
||||
- Angular version 16+
|
||||
|
||||
### Install
|
||||
|
||||
#### npm
|
||||
|
||||
```sh
|
||||
npm install --save @openfeature/angular-sdk
|
||||
```
|
||||
|
||||
#### yarn
|
||||
|
||||
```sh
|
||||
# yarn requires manual installation of the peer dependencies (see below)
|
||||
yarn add @openfeature/angular-sdk @openfeature/web-sdk @openfeature/core
|
||||
```
|
||||
|
||||
#### Required peer dependencies
|
||||
|
||||
The following list contains the peer dependencies of `@openfeature/angular-sdk`.
|
||||
See the [package.json](./package.json) for the required versions.
|
||||
|
||||
* `@openfeature/web-sdk`
|
||||
* `@angular/common`
|
||||
* `@angular/core`
|
||||
|
||||
### Usage
|
||||
|
||||
#### Module
|
||||
|
||||
To include the OpenFeature Angular directives in your application, you need to import the `OpenFeatureModule` and
|
||||
configure it using the `forRoot` method.
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { OpenFeatureModule } from '@openfeature/angular-sdk';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
// Other components
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
OpenFeatureModule.forRoot({
|
||||
provider: yourFeatureProvider,
|
||||
// domainBoundProviders are optional, mostly needed if more than one provider is used in the application.
|
||||
domainBoundProviders: {
|
||||
domain1: new YourOpenFeatureProvider(),
|
||||
domain2: new YourOtherOpenFeatureProvider(),
|
||||
},
|
||||
})
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
|
||||
##### Minimal Example
|
||||
|
||||
You don't need to provide all the templates. Here's a minimal example using a boolean feature flag:
|
||||
|
||||
If `initializing` and `reconciling` are not given, the feature flag value that is returned by the provider will
|
||||
determine what will be rendered.
|
||||
|
||||
```html
|
||||
<div *booleanFeatureFlag="'isFeatureEnabled'; default: true">
|
||||
This is shown when the feature flag is enabled.
|
||||
</div>
|
||||
```
|
||||
|
||||
This example shows content when the feature flag `isFeatureEnabled` is true with a default value of true.
|
||||
No `else`, `initializing`, or `reconciling` templates are required in this case.
|
||||
|
||||
#### How to use
|
||||
|
||||
The library provides four primary directives for feature flags, `booleanFeatureFlag`,
|
||||
`numberFeatureFlag`, `stringFeatureFlag` and `objectFeatureFlag`.
|
||||
|
||||
The first value given to the directive is the flag key that should be evaluated.
|
||||
|
||||
For all directives, the default value passed to OpenFeature has to be provided by the `default` parameter.
|
||||
|
||||
For all non-boolean directives, the value to compare the evaluation result to can be provided by the `value` parameter.
|
||||
This parameter is optional, if omitted, the `thenTemplate` will always be rendered.
|
||||
|
||||
The `domain` parameter is _optional_ and will be used as domain when getting the OpenFeature provider.
|
||||
|
||||
The `updateOnConfigurationChanged` and `updateOnContextChanged` parameter are _optional_ and used to disable the
|
||||
automatic re-rendering on flag value or context change. They are set to `true` by default.
|
||||
|
||||
The template referenced in `else` will be rendered if the evaluated feature flag is `false` for the `booleanFeatureFlag`
|
||||
directive and if the `value` does not match evaluated flag value for all other directives.
|
||||
This parameter is _optional_.
|
||||
|
||||
The template referenced in `initializing` and `reconciling` will be rendered if OpenFeature provider is in the
|
||||
corresponding states.
|
||||
This parameter is _optional_, if omitted, the `then` and `else` templates will be rendered according to the flag value.
|
||||
|
||||
##### Boolean Feature Flag
|
||||
|
||||
```html
|
||||
<div
|
||||
*booleanFeatureFlag="'isFeatureEnabled'; default: true; domain: 'userDomain'; else: booleanFeatureElse; initializing: booleanFeatureInitializing; reconciling: booleanFeatureReconciling">
|
||||
This is shown when the feature flag is enabled.
|
||||
</div>
|
||||
<ng-template #booleanFeatureElse>
|
||||
This is shown when the feature flag is disabled.
|
||||
</ng-template>
|
||||
<ng-template #booleanFeatureInitializing>
|
||||
This is shown when the feature flag is initializing.
|
||||
</ng-template>
|
||||
<ng-template #booleanFeatureReconciling>
|
||||
This is shown when the feature flag is reconciling.
|
||||
</ng-template>
|
||||
```
|
||||
|
||||
##### Number Feature Flag
|
||||
|
||||
```html
|
||||
<div
|
||||
*numberFeatureFlag="'discountRate'; value: 10; default: 5; domain: 'userDomain'; else: numberFeatureElse; initializing: numberFeatureInitializing; reconciling: numberFeatureReconciling">
|
||||
This is shown when the feature flag matches the specified discount rate.
|
||||
</div>
|
||||
<ng-template #numberFeatureElse>
|
||||
This is shown when the feature flag does not match the specified discount rate.
|
||||
</ng-template>
|
||||
<ng-template #numberFeatureInitializing>
|
||||
This is shown when the feature flag is initializing.
|
||||
</ng-template>
|
||||
<ng-template #numberFeatureReconciling>
|
||||
This is shown when the feature flag is reconciling.
|
||||
</ng-template>
|
||||
```
|
||||
|
||||
##### String Feature Flag
|
||||
|
||||
```html
|
||||
<div
|
||||
*stringFeatureFlag="'themeColor'; value: 'dark'; default: 'light'; domain: 'userDomain'; else: stringFeatureElse; initializing: stringFeatureInitializing; reconciling: stringFeatureReconciling">
|
||||
This is shown when the feature flag matches the specified theme color.
|
||||
</div>
|
||||
<ng-template #stringFeatureElse>
|
||||
This is shown when the feature flag does not match the specified theme color.
|
||||
</ng-template>
|
||||
<ng-template #stringFeatureInitializing>
|
||||
This is shown when the feature flag is initializing.
|
||||
</ng-template>
|
||||
<ng-template #stringFeatureReconciling>
|
||||
This is shown when the feature flag is reconciling.
|
||||
</ng-template>
|
||||
```
|
||||
|
||||
##### Object Feature Flag
|
||||
|
||||
```html
|
||||
<div
|
||||
*objectFeatureFlag="'userConfig'; value: { theme: 'dark' }; default: { theme: 'light' }; domain: 'userDomain'; else: objectFeatureElse; initializing: objectFeatureInitializing; reconciling: objectFeatureReconciling">
|
||||
This is shown when the feature flag matches the specified user configuration.
|
||||
</div>
|
||||
<ng-template #objectFeatureElse>
|
||||
This is shown when the feature flag does not match the specified user configuration.
|
||||
</ng-template>
|
||||
<ng-template #objectFeatureInitializing>
|
||||
This is shown when the feature flag is initializing.
|
||||
</ng-template>
|
||||
<ng-template #objectFeatureReconciling>
|
||||
This is shown when the feature flag is reconciling.
|
||||
</ng-template>
|
||||
```
|
||||
|
||||
##### Opting-out of automatic re-rendering
|
||||
|
||||
By default, the directive re-renders when the flag value changes or the context changes.
|
||||
|
||||
In cases, this is not desired, re-rendering can be disabled for both events:
|
||||
|
||||
```html
|
||||
<div *booleanFeatureFlag="'isFeatureEnabled'; default: true; updateOnContextChanged: false; updateOnConfigurationChanged: false;">
|
||||
This is shown when the feature flag is enabled.
|
||||
</div>
|
||||
```
|
||||
|
||||
##### Consuming the evaluation details
|
||||
|
||||
The `evaluation details` can be used when rendering the templates.
|
||||
The directives [`$implicit`](https://angular.dev/guide/directives/structural-directives#structural-directive-shorthand)
|
||||
value will be bound to the flag value and additionally the value `evaluationDetails` will be
|
||||
bound to the whole evaluation details.
|
||||
They can be referenced in all templates.
|
||||
|
||||
The following example shows `value` being implicitly bound and `details` being bound to the evaluation details.
|
||||
|
||||
```html
|
||||
<div
|
||||
*stringFeatureFlag="'themeColor'; value: 'dark'; default: 'light'; else: stringFeatureElse; let value; let details = evaluationDetails">
|
||||
It was a match!
|
||||
The theme color is {{ value }} because of {{ details.reason }}
|
||||
</div>
|
||||
<ng-template #stringFeatureElse let-value let-details='evaluationDetails'>
|
||||
It was no match!
|
||||
The theme color is {{ value }} because of {{ details.reason }}
|
||||
</ng-template>
|
||||
```
|
||||
|
||||
When the expected flag value is omitted, the template will always be rendered.
|
||||
This can be used to just render the flag value or details without conditional rendering.
|
||||
|
||||
```html
|
||||
<div *stringFeatureFlag="'themeColor'; default: 'light'; let value;">
|
||||
The theme color is {{ value }}.
|
||||
</div>
|
||||
```
|
||||
|
||||
##### Setting evaluation context
|
||||
|
||||
To set the initial evaluation context, you can add the `context` parameter to the `OpenFeatureModule` configuration.
|
||||
This context can be either an object or a factory function that returns an `EvaluationContext`.
|
||||
|
||||
> [!TIP]
|
||||
> Updating the context can be done directly via the global OpenFeature API using `OpenFeature.setContext()`
|
||||
|
||||
Here’s how you can define and use the initial client evaluation context:
|
||||
|
||||
###### Using a static object
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { OpenFeatureModule } from '@openfeature/angular-sdk';
|
||||
|
||||
const initialContext = {
|
||||
user: {
|
||||
id: 'user123',
|
||||
role: 'admin',
|
||||
}
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
OpenFeatureModule.forRoot({
|
||||
provider: yourFeatureProvider,
|
||||
context: initialContext
|
||||
})
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
###### Using a factory function
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { OpenFeatureModule, EvaluationContext } from '@openfeature/angular-sdk';
|
||||
|
||||
const contextFactory = (): EvaluationContext => loadContextFromLocalStorage();
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
OpenFeatureModule.forRoot({
|
||||
provider: yourFeatureProvider,
|
||||
context: contextFactory
|
||||
})
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## FAQ and troubleshooting
|
||||
|
||||
> I can import things form the `@openfeature/angular-sdk`, `@openfeature/web-sdk`, and `@openfeature/core`; which should I use?
|
||||
|
||||
The `@openfeature/angular-sdk` re-exports everything from its peers (`@openfeature/web-sdk` and `@openfeature/core`), and adds the Angular-specific features.
|
||||
You can import everything from the `@openfeature/angular-sdk` directly.
|
||||
Avoid importing anything from `@openfeature/web-sdk` or `@openfeature/core`.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Example repo](https://github.com/open-feature/angular-test-app)
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/angular",
|
||||
"keepLifecycleScripts": true,
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"name": "@openfeature/angular-sdk",
|
||||
"version": "0.0.16",
|
||||
"description": "OpenFeature Angular SDK",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/open-feature/js-sdk.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/open-feature/js-sdk/issues"
|
||||
},
|
||||
"homepage": "https://github.com/open-feature/js-sdk#readme",
|
||||
"scripts": {
|
||||
"current-published-version": "npm show $npm_package_name@$npm_package_version version",
|
||||
"current-version": "echo $npm_package_version",
|
||||
"prepack": "shx cp ./../../../../LICENSE ./LICENSE"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/core": "^16.2.12 || ^17.3.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@openfeature/web-sdk": "^1.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openfeature/core": "^1.8.1",
|
||||
"@openfeature/web-sdk": "^1.5.0",
|
||||
"@angular/common": "^20.1.2",
|
||||
"@angular/core": "^20.1.2"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
"openfeature",
|
||||
"feature",
|
||||
"flags",
|
||||
"toggles",
|
||||
"browser",
|
||||
"web",
|
||||
"angular"
|
||||
]
|
||||
}
|
|
@ -1,608 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { OpenFeatureModule } from './open-feature.module';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Client, ClientProviderEvents, FlagValue, InMemoryProvider, OpenFeature } from '@openfeature/web-sdk';
|
||||
import { TestingProvider } from '../test/test.utils';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
BooleanFeatureFlagDirective,
|
||||
NumberFeatureFlagDirective,
|
||||
ObjectFeatureFlagDirective,
|
||||
StringFeatureFlagDirective,
|
||||
} from './feature-flag.directive';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
BooleanFeatureFlagDirective,
|
||||
NumberFeatureFlagDirective,
|
||||
StringFeatureFlagDirective,
|
||||
ObjectFeatureFlagDirective,
|
||||
],
|
||||
template: `
|
||||
<ng-container>
|
||||
<div class="case-1">
|
||||
<div *booleanFeatureFlag="'test-flag'; default: true; domain: domain" class="flag-status">Flag On</div>
|
||||
</div>
|
||||
<div class="case-2">
|
||||
<div *booleanFeatureFlag="'test-flag'; default: true; else: elseTemplate; domain: domain" class="flag-status">
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-3">
|
||||
<div
|
||||
*booleanFeatureFlag="'test-flag'; default: false; initializing: initializingTemplate; domain: domain"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #initializingTemplate>
|
||||
<div class="flag-status">Initializing</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-4">
|
||||
<div
|
||||
*booleanFeatureFlag="'test-flag'; default: false; reconciling: reconcilingTemplate; domain: domain"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #reconcilingTemplate>
|
||||
<div class="flag-status">Reconciling</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-5">
|
||||
<div
|
||||
*booleanFeatureFlag="
|
||||
'test-flag';
|
||||
default: false;
|
||||
else: elseTemplate;
|
||||
initializing: initializingTemplate;
|
||||
reconciling: reconcilingTemplate;
|
||||
domain: domain
|
||||
"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
<ng-template #initializingTemplate>
|
||||
<div class="flag-status">Initializing</div>
|
||||
</ng-template>
|
||||
<ng-template #reconcilingTemplate>
|
||||
<div class="flag-status">Reconciling</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-6">
|
||||
<div
|
||||
*booleanFeatureFlag="specialFlagKey; default: true; else: elseTemplate; domain: domain"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-7">
|
||||
<div
|
||||
*numberFeatureFlag="'test-flag'; default: 0; value: 1; else: elseTemplate; domain: domain"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-8">
|
||||
<div
|
||||
*stringFeatureFlag="'test-flag'; default: 'default'; value: 'on'; else: elseTemplate; domain: domain"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-9">
|
||||
<div
|
||||
*objectFeatureFlag="
|
||||
'test-flag';
|
||||
default: {};
|
||||
value: { prop2: true, prop1: true };
|
||||
else: elseTemplate;
|
||||
domain: domain
|
||||
"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-10">
|
||||
<div
|
||||
*booleanFeatureFlag="
|
||||
'test-flag';
|
||||
default: false;
|
||||
domain: domain;
|
||||
else: elseTemplateWithContext;
|
||||
let value;
|
||||
let evaluationDetails = evaluationDetails
|
||||
"
|
||||
class="flag-status"
|
||||
>
|
||||
then {{ value }} {{ evaluationDetails.reason }}
|
||||
</div>
|
||||
<ng-template #elseTemplateWithContext let-value let-evaluationDetails="evaluationDetails">
|
||||
<div class="flag-status">else {{ value }} {{ evaluationDetails.reason }}</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="case-11">
|
||||
<div
|
||||
*stringFeatureFlag="'test-flag'; default: 'default'; domain: domain; let value = $implicit"
|
||||
class="flag-status"
|
||||
>
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-12">
|
||||
<div
|
||||
*booleanFeatureFlag="
|
||||
'test-flag';
|
||||
default: true;
|
||||
else: elseTemplate;
|
||||
domain: domain;
|
||||
updateOnConfigurationChanged: false
|
||||
"
|
||||
class="flag-status"
|
||||
>
|
||||
Flag On
|
||||
</div>
|
||||
<ng-template #elseTemplate>
|
||||
<div class="flag-status">Flag Off</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
`,
|
||||
})
|
||||
class TestComponent {
|
||||
@Input() domain: string;
|
||||
@Input() specialFlagKey: string = 'test-flag';
|
||||
protected readonly JSON = JSON;
|
||||
}
|
||||
|
||||
describe('FeatureFlagDirective', () => {
|
||||
describe('thenTemplate', () => {
|
||||
it('should not be rendered if disabled by the flag', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expectAmountElements(fixture, 'case-1', 0);
|
||||
});
|
||||
|
||||
it('should be rendered if enabled by the flag', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expectRenderedText(fixture, 'case-2', 'Flag On');
|
||||
});
|
||||
});
|
||||
|
||||
describe('elseTemplate', () => {
|
||||
it('should not be rendered if not existent but enabled by the flag', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expectAmountElements(fixture, 'case-1', 0);
|
||||
});
|
||||
|
||||
it('should not be rendered if existent but disabled by the flag', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-2', 'Flag On');
|
||||
});
|
||||
|
||||
it('should be rendered if existent and enabled by the flag', async () => {
|
||||
const { fixture, provider } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-2', 'Flag On');
|
||||
|
||||
await updateFlagValue(provider, false);
|
||||
fixture.detectChanges(); // Ensure change detection after flag update
|
||||
await expectRenderedText(fixture, 'case-2', 'Flag Off');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializingTemplate', () => {
|
||||
it('should not be rendered if provider is ready', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expectRenderedText(fixture, 'case-3', 'Flag On');
|
||||
});
|
||||
|
||||
it('should be rendered if provider is not ready', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
providerInitDelay: 1000,
|
||||
});
|
||||
|
||||
await expectRenderedText(fixture, 'case-3', 'Initializing');
|
||||
});
|
||||
|
||||
it('should render until the provider is initialized', async () => {
|
||||
const { fixture, client } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
providerInitDelay: 1000,
|
||||
});
|
||||
|
||||
await expectRenderedText(fixture, 'case-3', 'Initializing');
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-3', 'Flag On');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconcilingTemplate', () => {
|
||||
it('should not be rendered if provider is ready', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expectRenderedText(fixture, 'case-3', 'Flag On');
|
||||
});
|
||||
|
||||
it('should be rendered while provider is reconciling', async () => {
|
||||
const { fixture, domain, client } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
providerInitDelay: 500,
|
||||
});
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-4', 'Flag On');
|
||||
|
||||
const setContextPromise = OpenFeature.setContext(domain, { newCtx: true });
|
||||
await expectRenderedText(fixture, 'case-4', 'Reconciling');
|
||||
|
||||
await setContextPromise;
|
||||
await expectRenderedText(fixture, 'case-4', 'Flag On');
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex case', () => {
|
||||
it('should use initializing, then, else and reconciling in one go', async () => {
|
||||
const { fixture, provider, client, domain } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
providerInitDelay: 500,
|
||||
});
|
||||
|
||||
// Initializing
|
||||
await expectRenderedText(fixture, 'case-5', 'Initializing');
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-5', 'Flag On');
|
||||
|
||||
// Updating
|
||||
await updateFlagValue(provider, false);
|
||||
await expectRenderedText(fixture, 'case-5', 'Flag Off');
|
||||
|
||||
// Reconciling
|
||||
const setContextPromise = OpenFeature.setContext(domain, { newCtx: true });
|
||||
await expectRenderedText(fixture, 'case-5', 'Reconciling');
|
||||
await setContextPromise;
|
||||
await expectRenderedText(fixture, 'case-5', 'Flag Off');
|
||||
|
||||
// Updating 2
|
||||
await updateFlagValue(provider, true);
|
||||
await expectRenderedText(fixture, 'case-5', 'Flag On');
|
||||
});
|
||||
|
||||
it('should evaluate on flag key change', async () => {
|
||||
const { fixture, client } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
'new-test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-6', 'Flag On');
|
||||
|
||||
fixture.componentRef.setInput('specialFlagKey', 'new-test-flag');
|
||||
await fixture.whenStable();
|
||||
|
||||
await expectRenderedText(fixture, 'case-6', 'Flag Off');
|
||||
});
|
||||
|
||||
it('should opt-out of re-rendering when flag value changes', async () => {
|
||||
const { fixture, client, provider } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
'new-test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-12', 'Flag On');
|
||||
|
||||
await updateFlagValue(provider, false);
|
||||
await expectRenderedText(fixture, 'case-12', 'Flag On');
|
||||
});
|
||||
|
||||
it('should evaluate on flag domain change', async () => {
|
||||
const { fixture, client } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForClientReady(client);
|
||||
await expectRenderedText(fixture, 'case-6', 'Flag On');
|
||||
|
||||
const newDomain = v4();
|
||||
const newProvider = new TestingProvider(
|
||||
{
|
||||
'test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
await OpenFeature.setProviderAndWait(newDomain, newProvider);
|
||||
|
||||
fixture.componentRef.setInput('domain', newDomain);
|
||||
await fixture.whenStable();
|
||||
|
||||
await expectRenderedText(fixture, 'case-6', 'Flag Off');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numberFeatureFlag', () => {
|
||||
it('should render thenTemplate on match and else elseTemplate ', async () => {
|
||||
const { fixture, provider } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: 1 },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-7', 'Flag On');
|
||||
|
||||
await updateFlagValue(provider, 2);
|
||||
await expectRenderedText(fixture, 'case-7', 'Flag Off');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringFeatureFlag', () => {
|
||||
it('should render thenTemplate on match and else elseTemplate ', async () => {
|
||||
const { fixture, provider } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: 'on' },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-8', 'Flag On');
|
||||
|
||||
await updateFlagValue(provider, 'another-value');
|
||||
await expectRenderedText(fixture, 'case-8', 'Flag Off');
|
||||
});
|
||||
});
|
||||
|
||||
describe('objectFeatureFlag', () => {
|
||||
it('should render thenTemplate on match and else elseTemplate', async () => {
|
||||
const { fixture, provider } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: { prop1: true, prop2: true } },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-9', 'Flag On');
|
||||
|
||||
await updateFlagValue(provider, { prop2: 'string' });
|
||||
await expectRenderedText(fixture, 'case-9', 'Flag Off');
|
||||
});
|
||||
});
|
||||
|
||||
describe('context', () => {
|
||||
it('should render thenTemplate from context', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: true },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-10', 'then true STATIC');
|
||||
});
|
||||
|
||||
it('should render elseTemplate from context', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: false },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-10', 'else false STATIC');
|
||||
});
|
||||
|
||||
it('should always render if no expected value is given', async () => {
|
||||
const { fixture } = await createTestingModule({
|
||||
flagConfiguration: {
|
||||
'test-flag': {
|
||||
variants: { default: 'flag-value' },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await expectRenderedText(fixture, 'case-11', 'flag-value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createTestingModule(config?: {
|
||||
flagConfiguration?: ConstructorParameters<typeof InMemoryProvider>[0];
|
||||
providerInitDelay?: number;
|
||||
}): Promise<{ fixture: ComponentFixture<TestComponent>; provider: TestingProvider; domain: string; client: Client }> {
|
||||
const domain = v4();
|
||||
const provider = new TestingProvider(config?.flagConfiguration ?? {}, config?.providerInitDelay ?? 0);
|
||||
|
||||
const fixture = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
OpenFeatureModule.forRoot({ provider: new InMemoryProvider(), domainBoundProviders: { [domain]: provider } }),
|
||||
TestComponent,
|
||||
],
|
||||
}).createComponent(TestComponent);
|
||||
|
||||
fixture.componentRef.setInput('domain', domain);
|
||||
await fixture.whenStable();
|
||||
|
||||
const client = OpenFeature.getClient(domain);
|
||||
if (!config.providerInitDelay) {
|
||||
await waitForClientReady(client);
|
||||
}
|
||||
|
||||
return { provider, domain, client, fixture };
|
||||
}
|
||||
|
||||
async function waitForClientReady(client: Client) {
|
||||
await new Promise((resolve) => client.addHandler(ClientProviderEvents.Ready, resolve));
|
||||
}
|
||||
|
||||
async function updateFlagValue<T extends FlagValue>(provider: TestingProvider, value: T) {
|
||||
await provider.putConfiguration({
|
||||
'test-flag': {
|
||||
variants: { default: value },
|
||||
defaultVariant: 'default',
|
||||
disabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getElements(fixture: ComponentFixture<TestComponent>, testCase: string) {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
return fixture.debugElement.queryAll(By.css(`.${testCase} .flag-status`));
|
||||
}
|
||||
|
||||
async function expectAmountElements(fixture: ComponentFixture<TestComponent>, testCase: string, amount: number) {
|
||||
const divElements = await getElements(fixture, testCase);
|
||||
expect(divElements.length).toEqual(amount);
|
||||
}
|
||||
|
||||
async function expectRenderedText(fixture: ComponentFixture<TestComponent>, testCase: string, rendered: string) {
|
||||
const divElements = await getElements(fixture, testCase);
|
||||
expect(divElements.length).toEqual(1);
|
||||
expect(divElements[0].nativeElement.textContent.trim()).toBe(rendered);
|
||||
}
|
|
@ -1,706 +0,0 @@
|
|||
import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
EmbeddedViewRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Client,
|
||||
ClientProviderEvents,
|
||||
ClientProviderStatus,
|
||||
EvaluationDetails,
|
||||
EventHandler,
|
||||
FlagValue,
|
||||
JsonValue,
|
||||
OpenFeature,
|
||||
} from '@openfeature/web-sdk';
|
||||
|
||||
class FeatureFlagDirectiveContext<T extends FlagValue> {
|
||||
$implicit!: T;
|
||||
evaluationDetails: EvaluationDetails<T>;
|
||||
|
||||
constructor(details: EvaluationDetails<T>) {
|
||||
this.$implicit = details.value;
|
||||
this.evaluationDetails = details;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[featureFlag]',
|
||||
})
|
||||
export abstract class FeatureFlagDirective<T extends FlagValue> implements OnInit, OnDestroy, OnChanges {
|
||||
protected _changeDetectorRef: ChangeDetectorRef;
|
||||
protected _viewContainerRef: ViewContainerRef;
|
||||
|
||||
protected _featureFlagDefault: T;
|
||||
protected _featureFlagDomain: string | undefined;
|
||||
|
||||
protected _featureFlagKey: string;
|
||||
protected _featureFlagValue?: T;
|
||||
|
||||
protected _client: Client;
|
||||
protected _lastEvaluationResult: EvaluationDetails<T>;
|
||||
|
||||
protected _readyHandler: EventHandler<ClientProviderEvents.Ready> | null = null;
|
||||
protected _flagChangeHandler: EventHandler<ClientProviderEvents.ConfigurationChanged> | null = null;
|
||||
protected _contextChangeHandler: EventHandler<ClientProviderEvents.Error> | null = null;
|
||||
protected _reconcilingHandler: EventHandler<ClientProviderEvents.Reconciling> | null = null;
|
||||
|
||||
protected _updateOnContextChanged: boolean = true;
|
||||
protected _updateOnConfigurationChanged: boolean = true;
|
||||
|
||||
protected _thenTemplateRef: TemplateRef<FeatureFlagDirectiveContext<T>> | null;
|
||||
protected _thenViewRef: EmbeddedViewRef<unknown> | null;
|
||||
|
||||
protected _elseTemplateRef: TemplateRef<FeatureFlagDirectiveContext<T>> | null;
|
||||
protected _elseViewRef: EmbeddedViewRef<unknown> | null;
|
||||
|
||||
protected _initializingTemplateRef: TemplateRef<FeatureFlagDirectiveContext<T>> | null;
|
||||
protected _initializingViewRef: EmbeddedViewRef<unknown> | null;
|
||||
|
||||
protected _reconcilingTemplateRef: TemplateRef<FeatureFlagDirectiveContext<T>> | null;
|
||||
protected _reconcilingViewRef: EmbeddedViewRef<unknown> | null;
|
||||
|
||||
protected constructor() {}
|
||||
|
||||
set featureFlagDomain(domain: string | undefined) {
|
||||
/**
|
||||
* We have to handle the change of the domain explicitly because we need to get a new client when the domain changes.
|
||||
* This can not be done if we simply relay the onChanges method.
|
||||
*/
|
||||
this._featureFlagDomain = domain;
|
||||
this.initClient();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initClient();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this._flagChangeHandler?.();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._client) {
|
||||
this.disposeClient(this._client);
|
||||
this._client = null;
|
||||
}
|
||||
}
|
||||
|
||||
private initClient(): void {
|
||||
if (this._client) {
|
||||
this.disposeClient(this._client);
|
||||
}
|
||||
this._client = OpenFeature.getClient(this._featureFlagDomain);
|
||||
|
||||
const baseHandler = () => {
|
||||
const result = this.getFlagDetails(this._featureFlagKey, this._featureFlagDefault);
|
||||
this.onFlagValue(result, this._client.providerStatus);
|
||||
};
|
||||
|
||||
this._flagChangeHandler = () => {
|
||||
if (this._updateOnConfigurationChanged) {
|
||||
baseHandler();
|
||||
}
|
||||
};
|
||||
|
||||
this._contextChangeHandler = () => {
|
||||
if (this._updateOnContextChanged) {
|
||||
baseHandler();
|
||||
}
|
||||
};
|
||||
|
||||
this._readyHandler = () => baseHandler();
|
||||
this._reconcilingHandler = () => baseHandler();
|
||||
|
||||
this._client.addHandler(ClientProviderEvents.ConfigurationChanged, this._flagChangeHandler);
|
||||
this._client.addHandler(ClientProviderEvents.ContextChanged, this._contextChangeHandler);
|
||||
this._client.addHandler(ClientProviderEvents.Ready, this._readyHandler);
|
||||
this._client.addHandler(ClientProviderEvents.Reconciling, this._reconcilingHandler);
|
||||
}
|
||||
|
||||
private disposeClient(client: Client) {
|
||||
if (this._contextChangeHandler()) {
|
||||
client.removeHandler(ClientProviderEvents.ContextChanged, this._contextChangeHandler);
|
||||
}
|
||||
|
||||
if (this._flagChangeHandler) {
|
||||
client.removeHandler(ClientProviderEvents.ConfigurationChanged, this._flagChangeHandler);
|
||||
}
|
||||
|
||||
if (this._readyHandler) {
|
||||
client.removeHandler(ClientProviderEvents.Ready, this._readyHandler);
|
||||
}
|
||||
|
||||
if (this._reconcilingHandler) {
|
||||
client.removeHandler(ClientProviderEvents.Reconciling, this._reconcilingHandler);
|
||||
}
|
||||
}
|
||||
|
||||
protected getFlagDetails(flagKey: string, defaultValue: T): EvaluationDetails<T> {
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return this._client.getBooleanDetails(flagKey, defaultValue) as EvaluationDetails<T>;
|
||||
} else if (typeof defaultValue === 'number') {
|
||||
return this._client.getNumberDetails(flagKey, defaultValue) as EvaluationDetails<T>;
|
||||
} else if (typeof defaultValue === 'string') {
|
||||
return this._client.getStringDetails(flagKey, defaultValue) as EvaluationDetails<T>;
|
||||
} else {
|
||||
return this._client.getObjectDetails(flagKey, defaultValue) as EvaluationDetails<T>;
|
||||
}
|
||||
}
|
||||
|
||||
protected onFlagValue(result: EvaluationDetails<T>, status: ClientProviderStatus): void {
|
||||
const shouldInitialize = this._initializingTemplateRef && status === ClientProviderStatus.NOT_READY;
|
||||
const shouldReconcile = this._reconcilingTemplateRef && status === ClientProviderStatus.RECONCILING;
|
||||
|
||||
const context = new FeatureFlagDirectiveContext(result);
|
||||
|
||||
const resultChanged = !deepEqual(this._lastEvaluationResult, result);
|
||||
const isValueMatch = !this._featureFlagValue || deepEqual(result.value, this._featureFlagValue);
|
||||
|
||||
if (this._initializingViewRef && shouldInitialize && !resultChanged) {
|
||||
return;
|
||||
} else if (this._reconcilingViewRef && shouldReconcile && !resultChanged) {
|
||||
return;
|
||||
} else if (this._thenViewRef && isValueMatch && !shouldInitialize && !shouldReconcile && !resultChanged) {
|
||||
return;
|
||||
} else if (this._elseViewRef && !isValueMatch && !shouldInitialize && !shouldReconcile && !resultChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastEvaluationResult = result;
|
||||
this._viewContainerRef.clear();
|
||||
this._initializingViewRef = null;
|
||||
this._reconcilingViewRef = null;
|
||||
this._thenViewRef = null;
|
||||
this._elseViewRef = null;
|
||||
|
||||
if (this._initializingTemplateRef && status === ClientProviderStatus.NOT_READY) {
|
||||
this._initializingViewRef = this._viewContainerRef.createEmbeddedView(this._initializingTemplateRef, context);
|
||||
} else if (this._reconcilingTemplateRef && status === ClientProviderStatus.RECONCILING) {
|
||||
this._reconcilingViewRef = this._viewContainerRef.createEmbeddedView(this._reconcilingTemplateRef, context);
|
||||
} else if (isValueMatch) {
|
||||
this._thenViewRef = this._viewContainerRef.createEmbeddedView(this._thenTemplateRef, context);
|
||||
} else if (this._elseTemplateRef) {
|
||||
this._elseViewRef = this._viewContainerRef.createEmbeddedView(this._elseTemplateRef, context);
|
||||
}
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural directive that conditionally includes a template based on the evaluation
|
||||
* of a boolean feature flag.
|
||||
* When the flag evaluates to true, Angular renders the template provided in a `then` clause,
|
||||
* and when false, Angular renders the template provided in an optional `else` clause.
|
||||
* The default template for the `else` clause is blank.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* ```
|
||||
* <div *booleanFeatureFlag="'flagKey'; default: false; let value">{{ value }}</div>
|
||||
* ```
|
||||
* ```
|
||||
* <div *booleanFeatureFlag="flagKey; default: false; else: elseTemplate">Content to render when flag is true.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag is false.</ng-template>
|
||||
* ```
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* You can specify templates for other statuses such as initializing and reconciling.
|
||||
*
|
||||
* ```
|
||||
* <div *booleanFeatureFlag="flagKey; default:true; else: elseTemplate; initializing: initializingTemplate; reconciling: reconcilingTemplate">Content to render when flag is true.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag is false.</ng-template>
|
||||
* <ng-template #initializingTemplate>Loading...</ng-template>
|
||||
* <ng-template #reconcilingTemplate>Reconfiguring...</ng-template>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[booleanFeatureFlag]',
|
||||
})
|
||||
export class BooleanFeatureFlagDirective extends FeatureFlagDirective<boolean> implements OnChanges {
|
||||
override _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
override _viewContainerRef = inject(ViewContainerRef);
|
||||
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<boolean>>>(TemplateRef);
|
||||
|
||||
/**
|
||||
* The key of the boolean feature flag.
|
||||
*/
|
||||
@Input({ required: true }) booleanFeatureFlag: string;
|
||||
|
||||
/**
|
||||
* The default value for the boolean feature flag.
|
||||
*/
|
||||
@Input({ required: true }) booleanFeatureFlagDefault: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override ngOnChanges() {
|
||||
this._featureFlagKey = this.booleanFeatureFlag;
|
||||
this._featureFlagDefault = this.booleanFeatureFlagDefault;
|
||||
this._featureFlagValue = true;
|
||||
super.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* The domain of the boolean feature flag.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set booleanFeatureFlagDomain(domain: string | undefined) {
|
||||
super.featureFlagDomain = domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component if the provider emits a ConfigurationChanged event.
|
||||
* Set to false to prevent components from re-rendering when flag value changes
|
||||
* are received by the associated provider.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set booleanFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) {
|
||||
this._updateOnConfigurationChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component when the OpenFeature context changes.
|
||||
* Set to false to prevent components from re-rendering when attributes which
|
||||
* may be factors in flag evaluation change.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set booleanFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) {
|
||||
this._updateOnContextChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is false.
|
||||
*/
|
||||
@Input()
|
||||
set booleanFeatureFlagElse(tpl: TemplateRef<FeatureFlagDirectiveContext<boolean>>) {
|
||||
this._elseTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the provider is not ready.
|
||||
*/
|
||||
@Input()
|
||||
set booleanFeatureFlagInitializing(tpl: TemplateRef<FeatureFlagDirectiveContext<boolean>>) {
|
||||
this._initializingTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the provider is reconciling.
|
||||
*/
|
||||
@Input()
|
||||
set booleanFeatureFlagReconciling(tpl: TemplateRef<FeatureFlagDirectiveContext<boolean>>) {
|
||||
this._reconcilingTemplateRef = tpl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural directive that conditionally includes a template based on the evaluation
|
||||
* of a number feature flag.
|
||||
* When the flag matches the provided value or no expected value is given, Angular renders the template provided
|
||||
* in a `then` clause, and when it doesn't match, Angular renders the template provided
|
||||
* in an optional `else` clause.
|
||||
* The default template for the `else` clause is blank.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* ```
|
||||
* <div *numberFeatureFlag="'flagKey'; default: 0; let value">{{ value }}</div>
|
||||
* ```
|
||||
* ```
|
||||
* <div *numberFeatureFlag="'flagKey'; value: 1; default: 0; else: elseTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* ```
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* You can specify templates for other statuses such as initializing and reconciling.
|
||||
*
|
||||
* ```
|
||||
* <div *numberFeatureFlag="flagKey; default: 0; value: flagValue; else: elseTemplate; initializing: initializingTemplate; reconciling: reconcilingTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* <ng-template #initializingTemplate>Loading...</ng-template>
|
||||
* <ng-template #reconcilingTemplate>Reconfiguring...</ng-template>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[numberFeatureFlag]',
|
||||
})
|
||||
export class NumberFeatureFlagDirective extends FeatureFlagDirective<number> implements OnChanges {
|
||||
override _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
override _viewContainerRef = inject(ViewContainerRef);
|
||||
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<number>>>(TemplateRef);
|
||||
|
||||
/**
|
||||
* The key of the number feature flag.
|
||||
*/
|
||||
@Input({ required: true }) numberFeatureFlag: string;
|
||||
|
||||
/**
|
||||
* The default value for the number feature flag.
|
||||
*/
|
||||
@Input({ required: true }) numberFeatureFlagDefault: number;
|
||||
|
||||
/**
|
||||
* The expected value of this number feature flag, for which the `then` template should be rendered.
|
||||
*/
|
||||
@Input({ required: false }) numberFeatureFlagValue?: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override ngOnChanges() {
|
||||
this._featureFlagKey = this.numberFeatureFlag;
|
||||
this._featureFlagDefault = this.numberFeatureFlagDefault;
|
||||
this._featureFlagValue = this.numberFeatureFlagValue;
|
||||
super.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* The domain of the number feature flag.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set numberFeatureFlagDomain(domain: string | undefined) {
|
||||
super.featureFlagDomain = domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component if the provider emits a ConfigurationChanged event.
|
||||
* Set to false to prevent components from re-rendering when flag value changes
|
||||
* are received by the associated provider.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set numberFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) {
|
||||
this._updateOnConfigurationChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component when the OpenFeature context changes.
|
||||
* Set to false to prevent components from re-rendering when attributes which
|
||||
* may be factors in flag evaluation change.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set numberFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) {
|
||||
this._updateOnContextChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag does not match value.
|
||||
*/
|
||||
@Input()
|
||||
set numberFeatureFlagElse(tpl: TemplateRef<FeatureFlagDirectiveContext<number>>) {
|
||||
this._elseTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is not ready.
|
||||
*/
|
||||
@Input()
|
||||
set numberFeatureFlagInitializing(tpl: TemplateRef<FeatureFlagDirectiveContext<number>>) {
|
||||
this._initializingTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is not ready.
|
||||
*/
|
||||
@Input()
|
||||
set numberFeatureFlagReconciling(tpl: TemplateRef<FeatureFlagDirectiveContext<number>>) {
|
||||
this._reconcilingTemplateRef = tpl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural directive that conditionally includes a template based on the evaluation
|
||||
* of a string feature flag.
|
||||
* When the flag matches the provided value or no expected value is given, Angular renders the template provided
|
||||
* in a `then` clause, and when it doesn't match, Angular renders the template provided
|
||||
* in an optional `else` clause.
|
||||
* The default template for the `else` clause is blank.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* ```
|
||||
* <div *stringFeatureFlag="'flagKey'; default: 'default'; let value">{{ value }}</div>
|
||||
* ```
|
||||
* ```
|
||||
* <div *stringFeatureFlag="flagKey; default: 'default'; value: flagValue; else: elseTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* ```
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* You can specify templates for other statuses such as initializing and reconciling.
|
||||
*
|
||||
* ```
|
||||
* <div *stringFeatureFlag="flagKey; default: 'default'; value: flagValue; else: elseTemplate; initializing: initializingTemplate; reconciling: reconcilingTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* <ng-template #initializingTemplate>Loading...</ng-template>
|
||||
* <ng-template #reconcilingTemplate>Reconfiguring...</ng-template>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[stringFeatureFlag]',
|
||||
})
|
||||
export class StringFeatureFlagDirective extends FeatureFlagDirective<string> implements OnChanges {
|
||||
override _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
override _viewContainerRef = inject(ViewContainerRef);
|
||||
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<string>>>(TemplateRef);
|
||||
|
||||
/**
|
||||
* The key of the string feature flag.
|
||||
*/
|
||||
@Input({ required: true }) stringFeatureFlag: string;
|
||||
|
||||
/**
|
||||
* The default value for the string feature flag.
|
||||
*/
|
||||
@Input({ required: true }) stringFeatureFlagDefault: string;
|
||||
|
||||
/**
|
||||
* The expected value of this string feature flag, for which the `then` template should be rendered.
|
||||
*/
|
||||
@Input({ required: false }) stringFeatureFlagValue?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override ngOnChanges() {
|
||||
this._featureFlagKey = this.stringFeatureFlag;
|
||||
this._featureFlagDefault = this.stringFeatureFlagDefault;
|
||||
this._featureFlagValue = this.stringFeatureFlagValue;
|
||||
super.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* The domain for the string feature flag.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set stringFeatureFlagDomain(domain: string | undefined) {
|
||||
super.featureFlagDomain = domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component if the provider emits a ConfigurationChanged event.
|
||||
* Set to false to prevent components from re-rendering when flag value changes
|
||||
* are received by the associated provider.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set stringFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) {
|
||||
this._updateOnConfigurationChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component when the OpenFeature context changes.
|
||||
* Set to false to prevent components from re-rendering when attributes which
|
||||
* may be factors in flag evaluation change.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set stringFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) {
|
||||
this._updateOnContextChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag does not match value.
|
||||
*/
|
||||
@Input()
|
||||
set stringFeatureFlagElse(tpl: TemplateRef<FeatureFlagDirectiveContext<string>>) {
|
||||
this._elseTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is not ready.
|
||||
*/
|
||||
@Input()
|
||||
set stringFeatureFlagInitializing(tpl: TemplateRef<FeatureFlagDirectiveContext<string>>) {
|
||||
this._initializingTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is reconciling.
|
||||
*/
|
||||
@Input()
|
||||
set stringFeatureFlagReconciling(tpl: TemplateRef<FeatureFlagDirectiveContext<string>>) {
|
||||
this._reconcilingTemplateRef = tpl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural directive that conditionally includes a template based on the evaluation
|
||||
* of an object feature flag.
|
||||
* When the flag matches the provided value or no expected value is given, Angular renders the template provided
|
||||
* in a `then` clause, and when it doesn't match, Angular renders the template provided
|
||||
* in an optional `else` clause.
|
||||
* The default template for the `else` clause is blank.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* ```
|
||||
* <div *objectFeatureFlag="'flagKey'; default: {}; let value">{{ value }}</div>
|
||||
* ```
|
||||
* ```
|
||||
* <div *objectFeatureFlag="flagKey; default: {}; value: flagValue; else: elseTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* ```
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* You can specify templates for other statuses such as initializing and reconciling.
|
||||
*
|
||||
* ```
|
||||
* <div *objectFeatureFlag="flagKey; default: {}; value: flagValue; else: elseTemplate; initializing: initializingTemplate; reconciling: reconcilingTemplate">Content to render when flag matches value.</div>
|
||||
* <ng-template #elseTemplate>Content to render when flag does not match value.</ng-template>
|
||||
* <ng-template #initializingTemplate>Loading...</ng-template>
|
||||
* <ng-template #reconcilingTemplate>Reconfiguring...</ng-template>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
standalone: true,
|
||||
selector: '[objectFeatureFlag]',
|
||||
})
|
||||
export class ObjectFeatureFlagDirective<T extends JsonValue> extends FeatureFlagDirective<T> implements OnChanges {
|
||||
override _changeDetectorRef = inject(ChangeDetectorRef);
|
||||
override _viewContainerRef = inject(ViewContainerRef);
|
||||
override _thenTemplateRef = inject<TemplateRef<FeatureFlagDirectiveContext<T>>>(TemplateRef);
|
||||
|
||||
/**
|
||||
* The key of the object feature flag.
|
||||
*/
|
||||
@Input({ required: true }) objectFeatureFlag: string;
|
||||
|
||||
/**
|
||||
* The default value for the object feature flag.
|
||||
*/
|
||||
@Input({ required: true }) objectFeatureFlagDefault: T;
|
||||
|
||||
/**
|
||||
* The expected value of this object feature flag, for which the `then` template should be rendered.
|
||||
*/
|
||||
@Input({ required: false }) objectFeatureFlagValue?: T;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override ngOnChanges() {
|
||||
this._featureFlagKey = this.objectFeatureFlag;
|
||||
this._featureFlagDefault = this.objectFeatureFlagDefault;
|
||||
this._featureFlagValue = this.objectFeatureFlagValue;
|
||||
super.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* The domain for the object feature flag.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set objectFeatureFlagDomain(domain: string | undefined) {
|
||||
super.featureFlagDomain = domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component if the provider emits a ConfigurationChanged event.
|
||||
* Set to false to prevent components from re-rendering when flag value changes
|
||||
* are received by the associated provider.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set objectFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) {
|
||||
this._updateOnConfigurationChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the component when the OpenFeature context changes.
|
||||
* Set to false to prevent components from re-rendering when attributes which
|
||||
* may be factors in flag evaluation change.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@Input({ required: false })
|
||||
set objectFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) {
|
||||
this._updateOnContextChanged = enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag does not match value.
|
||||
*/
|
||||
@Input()
|
||||
set objectFeatureFlagElse(tpl: TemplateRef<FeatureFlagDirectiveContext<T>>) {
|
||||
this._elseTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is not ready.
|
||||
*/
|
||||
@Input()
|
||||
set objectFeatureFlagInitializing(tpl: TemplateRef<FeatureFlagDirectiveContext<T>>) {
|
||||
this._initializingTemplateRef = tpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template to be displayed when the feature flag is reconciling.
|
||||
*/
|
||||
@Input()
|
||||
set objectFeatureFlagReconciling(tpl: TemplateRef<FeatureFlagDirectiveContext<T>>) {
|
||||
this._reconcilingTemplateRef = tpl;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function deepEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) {
|
||||
// If both objects are identical
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
|
||||
// One of them is not an object or one of them is null
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
// Different number of properties
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!keys2.includes(key)) {
|
||||
// obj2 does not have a property that obj1 has
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recursive check for each property
|
||||
if (!deepEqual(obj1[key], obj2[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { EvaluationContext, OpenFeature, Provider } from '@openfeature/web-sdk';
|
||||
|
||||
export type EvaluationContextFactory = () => EvaluationContext;
|
||||
|
||||
export interface OpenFeatureConfig {
|
||||
provider: Provider;
|
||||
domainBoundProviders?: Record<string, Provider>;
|
||||
context?: EvaluationContext | EvaluationContextFactory;
|
||||
}
|
||||
|
||||
export const OPEN_FEATURE_CONFIG_TOKEN = new InjectionToken<OpenFeatureConfig>('OPEN_FEATURE_CONFIG_TOKEN');
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [CommonModule],
|
||||
exports: [],
|
||||
})
|
||||
export class OpenFeatureModule {
|
||||
static forRoot(config: OpenFeatureConfig): ModuleWithProviders<OpenFeatureModule> {
|
||||
const context = typeof config.context === 'function' ? config.context() : config.context;
|
||||
OpenFeature.setProvider(config.provider, context);
|
||||
|
||||
if (config.domainBoundProviders) {
|
||||
Object.entries(config.domainBoundProviders).map(([domain, provider]) =>
|
||||
OpenFeature.setProvider(domain, provider),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
ngModule: OpenFeatureModule,
|
||||
providers: [{ provide: OPEN_FEATURE_CONFIG_TOKEN, useValue: config }],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Public API Surface of angular
|
||||
*/
|
||||
|
||||
export * from './lib/feature-flag.directive';
|
||||
export * from './lib/open-feature.module';
|
||||
|
||||
// re-export the web-sdk so consumers can access that API from the angular-sdk
|
||||
export * from '@openfeature/web-sdk';
|
|
@ -1,3 +0,0 @@
|
|||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
|
||||
export default [provideZonelessChangeDetection()];
|
|
@ -1,20 +0,0 @@
|
|||
import { InMemoryProvider } from '@openfeature/web-sdk';
|
||||
|
||||
export class TestingProvider extends InMemoryProvider {
|
||||
constructor(
|
||||
flagConfiguration: ConstructorParameters<typeof InMemoryProvider>[0],
|
||||
private delay: number,
|
||||
) {
|
||||
super(flagConfiguration);
|
||||
}
|
||||
|
||||
// artificially delay our init (delaying PROVIDER_READY event)
|
||||
async initialize(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.delay));
|
||||
}
|
||||
|
||||
// artificially delay context changes
|
||||
async onContextChange(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.delay));
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": [],
|
||||
"paths": {
|
||||
"angular": [
|
||||
"./dist/angular"
|
||||
],
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"paths": {
|
||||
"angular": [
|
||||
"./dist/angular"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"node"
|
||||
],
|
||||
"paths": {
|
||||
"angular": [
|
||||
"./dist/angular"
|
||||
]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/test-provider.ts"
|
||||
]
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"angular": [
|
||||
"./dist/angular"
|
||||
],
|
||||
"@openfeature/core": [ "../shared/src" ],
|
||||
"@openfeature/web-sdk": [ "../web/src" ]
|
||||
},
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"strictNullChecks": false,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
],
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"disableTypeScriptVersionCheck": true,
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"node"
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"projects/angular-sdk/src/**/*.spec.ts",
|
||||
"projects/angular-sdk/src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,233 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [1.6.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.6.0...web-sdk-v1.6.1) (2025-08-14)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* update core dep ([#1228](https://github.com/open-feature/js-sdk/issues/1228)) ([845d24c](https://github.com/open-feature/js-sdk/commit/845d24c5fecc80de3080e49fde839f08ecac6b33))
|
||||
|
||||
## [1.6.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.5.0...web-sdk-v1.6.0) (2025-08-12)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add evaluation-scoped hook data ([#1216](https://github.com/open-feature/js-sdk/issues/1216)) ([07af3a9](https://github.com/open-feature/js-sdk/commit/07af3a9eda895e9edb24c7ee1e3c1c4f16e17431))
|
||||
* **web-global-build:** impl ([#1225](https://github.com/open-feature/js-sdk/issues/1225)) ([40a512e](https://github.com/open-feature/js-sdk/commit/40a512e21204eb92dc3ef4161b383f9c1fd74da7))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* Clarify the behavior of setProviderAndWait ([#1180](https://github.com/open-feature/js-sdk/issues/1180)) ([4fe8d87](https://github.com/open-feature/js-sdk/commit/4fe8d87a2e5df2cbd4086cc4f4a380e8857ed8ba))
|
||||
|
||||
## [1.5.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.4.1...web-sdk-v1.5.0) (2025-04-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add a top-level method for accessing providers ([#1152](https://github.com/open-feature/js-sdk/issues/1152)) ([ae8fce8](https://github.com/open-feature/js-sdk/commit/ae8fce87530005ed20f7e68dc696ce67053fca31))
|
||||
* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* Typo in name of the function ([2c5b37c](https://github.com/open-feature/js-sdk/commit/2c5b37c79d72d60864c27b9e67d96e99ef4ae241))
|
||||
|
||||
## [1.4.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.4.0...web-sdk-v1.4.1) (2025-02-07)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* msg missing when providers return err resolutions ([#1134](https://github.com/open-feature/js-sdk/issues/1134)) ([bc9f6e4](https://github.com/open-feature/js-sdk/commit/bc9f6e44da3f1c0a66659aee2d0316629ac34fbf))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* update core peer ([8bbd43e](https://github.com/open-feature/js-sdk/commit/8bbd43e579a0c2e0c5b7eec00f94bbcffce04773))
|
||||
|
||||
## [1.4.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.3.2...web-sdk-v1.4.0) (2024-12-18)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
The signature of the `finally` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). Note that since hooks are still `experimental,` this does not constitute a change requiring a new major version. To migrate, update any hook that implements the `finally` stage to accept `evaluation details` as the second argument.
|
||||
|
||||
* add evaluation details to finally hook ([#1087](https://github.com/open-feature/js-sdk/issues/1087)) ([2135254](https://github.com/open-feature/js-sdk/commit/2135254c4bee52b4bcadfbf8b99a896cfd930cca))
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add evaluation details to finally hook ([#1087](https://github.com/open-feature/js-sdk/issues/1087)) ([2135254](https://github.com/open-feature/js-sdk/commit/2135254c4bee52b4bcadfbf8b99a896cfd930cca))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* fix comment in README for Hook’s after method ([#1102](https://github.com/open-feature/js-sdk/issues/1102)) ([ba8d1ae](https://github.com/open-feature/js-sdk/commit/ba8d1aeec837cb089cda3499d44ecc505ea0c947))
|
||||
|
||||
|
||||
### 🔄 Refactoring
|
||||
|
||||
* improve track interface for providers ([#1100](https://github.com/open-feature/js-sdk/issues/1100)) ([5e5b160](https://github.com/open-feature/js-sdk/commit/5e5b16022122b71760634ac90e3fd962aa831c74))
|
||||
|
||||
## [1.3.2](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.3.1...web-sdk-v1.3.2) (2024-11-07)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* update OpenFeature core version to 1.5.0 ([#1077](https://github.com/open-feature/js-sdk/issues/1077)) ([a3469b6](https://github.com/open-feature/js-sdk/commit/a3469b6799c3f0d77cb38dcdc708ce615123d7f6))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* loosen peer dependency requirements, remove some ci automation ([#1080](https://github.com/open-feature/js-sdk/issues/1080)) ([ef3ba21](https://github.com/open-feature/js-sdk/commit/ef3ba2167ac95cd0c6a046d206bd60bbcf84e80c))
|
||||
|
||||
|
||||
### 🚀 Performance
|
||||
|
||||
* avoid using exceptions for flow control ([#1074](https://github.com/open-feature/js-sdk/issues/1074)) ([26264d6](https://github.com/open-feature/js-sdk/commit/26264d6d090b2ed31b27d36e71194b9fa911563b))
|
||||
|
||||
## [1.3.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.3.0...web-sdk-v1.3.1) (2024-10-29)
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* add tracking sections ([#1068](https://github.com/open-feature/js-sdk/issues/1068)) ([e131faf](https://github.com/open-feature/js-sdk/commit/e131faffad9025e9c7194f39558bf3b3cec31807))
|
||||
|
||||
## [1.3.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.2.4...web-sdk-v1.3.0) (2024-10-29)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* implement tracking as per spec ([#1020](https://github.com/open-feature/js-sdk/issues/1020)) ([80f182e](https://github.com/open-feature/js-sdk/commit/80f182e1afbd3a705bf3de6a0d9886ccb3424b44))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* import type lint rule and fixes ([#1039](https://github.com/open-feature/js-sdk/issues/1039)) ([01fcb93](https://github.com/open-feature/js-sdk/commit/01fcb933d2cbd131a0f4a005173cdd1906087e18))
|
||||
* **main:** release core 1.5.0 ([#1040](https://github.com/open-feature/js-sdk/issues/1040)) ([fe3ad8e](https://github.com/open-feature/js-sdk/commit/fe3ad8eeb9219ff08ba287cab228016da0b88e88))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* update domain context terminology ([#1037](https://github.com/open-feature/js-sdk/issues/1037)) ([924802b](https://github.com/open-feature/js-sdk/commit/924802b21d70889631e1fb0fb02225a7f8d2638d))
|
||||
|
||||
## [1.2.4](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.2.3...web-sdk-v1.2.4) (2024-09-20)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **web:** bump core peer version ([#1018](https://github.com/open-feature/js-sdk/issues/1018)) ([970335e](https://github.com/open-feature/js-sdk/commit/970335e92bbaa7bf093120da3fab03659b0c11bf))
|
||||
|
||||
## [1.2.3](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.2.2...web-sdk-v1.2.3) (2024-08-28)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 1.4.0 ([#984](https://github.com/open-feature/js-sdk/issues/984)) ([01344b2](https://github.com/open-feature/js-sdk/commit/01344b28c1381d9de3aefde89be841b597a00b70))
|
||||
* move client/ dir to web/ ([#991](https://github.com/open-feature/js-sdk/issues/991)) ([df4e72e](https://github.com/open-feature/js-sdk/commit/df4e72eabc3370801303470ca37263a0d4d9bb38))
|
||||
|
||||
## [1.2.2](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.2.1...web-sdk-v1.2.2) (2024-08-22)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* race condition in test provider with suspense ([#980](https://github.com/open-feature/js-sdk/issues/980)) ([0f187fe](https://github.com/open-feature/js-sdk/commit/0f187fe0b584e66b6283531eb7879c320967f921))
|
||||
|
||||
## [1.2.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.2.0...web-sdk-v1.2.1) (2024-06-12)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **web-sdk:** pin core version to 1.3.0 ([#964](https://github.com/open-feature/js-sdk/issues/964)) ([3cde37a](https://github.com/open-feature/js-sdk/commit/3cde37a5ee29e71a0eb3fd8680b081d865a588f9))
|
||||
|
||||
## [1.2.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.1.0...web-sdk-v1.2.0) (2024-06-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* lower compilation target to es2015 ([#957](https://github.com/open-feature/js-sdk/issues/957)) ([c2d6c17](https://github.com/open-feature/js-sdk/commit/c2d6c1761ae19f937deaff2f011a0380f8af7350))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 1.3.0 ([#958](https://github.com/open-feature/js-sdk/issues/958)) ([25086c5](https://github.com/open-feature/js-sdk/commit/25086c5456d81fa040ce95ea1a067543408e3150))
|
||||
|
||||
## [1.1.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.0.3...web-sdk-v1.1.0) (2024-05-13)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* set context during provider init on web ([#919](https://github.com/open-feature/js-sdk/issues/919)) ([7e6c1c6](https://github.com/open-feature/js-sdk/commit/7e6c1c6e7082e75535bf81b4e70c8c57ef870b77))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* remove export of OpenFeatureClient ([#794](https://github.com/open-feature/js-sdk/issues/794)) ([3d197f2](https://github.com/open-feature/js-sdk/commit/3d197f2ea74f00ef904fc6a6960160d0cf4ded9a))
|
||||
* removes exports of OpenFeatureClient class and makes event props readonly ([#918](https://github.com/open-feature/js-sdk/issues/918)) ([e9a25c2](https://github.com/open-feature/js-sdk/commit/e9a25c21cb17c3b5700bca652e3c0ed15e8f49b4))
|
||||
* run error hook when provider returns reason error or error code ([#926](https://github.com/open-feature/js-sdk/issues/926)) ([c6d0b5d](https://github.com/open-feature/js-sdk/commit/c6d0b5da9c7f4c11319422fbe8c668a7613b044d))
|
||||
* skip reconciling event for synchronous onContextChange operations ([#931](https://github.com/open-feature/js-sdk/issues/931)) ([6c25f29](https://github.com/open-feature/js-sdk/commit/6c25f29f11ddb9d4ee617f1ed3f1d26be4f554ac))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 1.2.0 ([#927](https://github.com/open-feature/js-sdk/issues/927)) ([692ad5b](https://github.com/open-feature/js-sdk/commit/692ad5b27a052a4c5abba81fe1caa071edd59ee7))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* add tip about supported usage in the install section ([#941](https://github.com/open-feature/js-sdk/issues/941)) ([f0de667](https://github.com/open-feature/js-sdk/commit/f0de66770be778d7a51063e706c9cccbba4b214e))
|
||||
|
||||
## [1.0.3](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.0.2...web-sdk-v1.0.3) (2024-04-18)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* bump spec version badge to v0.8.0 ([#910](https://github.com/open-feature/js-sdk/issues/910)) ([a7b2c4b](https://github.com/open-feature/js-sdk/commit/a7b2c4bca09112d49e637735466502adb1438ebe))
|
||||
|
||||
## [1.0.2](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.0.1...web-sdk-v1.0.2) (2024-04-02)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* return metadata for the bound provider in hookContext ([#883](https://github.com/open-feature/js-sdk/issues/883)) ([fd84025](https://github.com/open-feature/js-sdk/commit/fd84025bdfe30e8d730fa546d01c1ad6c6953189))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 1.1.0 ([#899](https://github.com/open-feature/js-sdk/issues/899)) ([b3e5f7e](https://github.com/open-feature/js-sdk/commit/b3e5f7eb2aac5d5533c51764242e06a6ba508082))
|
||||
|
||||
## [1.0.1](https://github.com/open-feature/js-sdk/compare/web-sdk-v1.0.0...web-sdk-v1.0.1) (2024-03-25)
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* add peer dep explainer ([#876](https://github.com/open-feature/js-sdk/issues/876)) ([cfd23b9](https://github.com/open-feature/js-sdk/commit/cfd23b90f0ca2673253fbbe30f4db585e746bc63))
|
||||
|
||||
## [1.0.0](https://github.com/open-feature/js-sdk/compare/web-sdk-v0.4.16...web-sdk-v1.0.0) (2024-03-13)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* prompt web-sdk 1.0 ([#871](https://github.com/open-feature/js-sdk/issues/871)) ([7d50d93](https://github.com/open-feature/js-sdk/commit/7d50d931d5cda349a31969c997e7581ea4883b6a))
|
||||
|
||||
## [0.4.16](https://github.com/open-feature/js-sdk/compare/web-sdk-v0.4.15...web-sdk-v0.4.16) (2024-03-12)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 1.0.0 ([#869](https://github.com/open-feature/js-sdk/issues/869)) ([4191a02](https://github.com/open-feature/js-sdk/commit/4191a02dbc5b66053b63d19e2e9c5bf750aaf4bf))
|
||||
|
||||
## [0.4.15](https://github.com/open-feature/js-sdk/compare/web-sdk-v0.4.14...web-sdk-v0.4.15) (2024-03-05)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* use EvenEmitter3 for web-sdk ([#847](https://github.com/open-feature/js-sdk/issues/847)) ([861cf83](https://github.com/open-feature/js-sdk/commit/861cf8378271daf6205c5fc199ffc1bde8dfcc64))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **main:** release core 0.0.28 ([#849](https://github.com/open-feature/js-sdk/issues/849)) ([31b92a9](https://github.com/open-feature/js-sdk/commit/31b92a97c19071334cb7cf10767be9d40be55943))
|
||||
|
||||
## [0.4.14](https://github.com/open-feature/js-sdk/compare/web-sdk-v0.4.13...web-sdk-v0.4.14) (2024-03-05)
|
||||
|
||||
|
|
@ -12,15 +12,18 @@
|
|||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-start-version -->
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/web-sdk-v1.6.1">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.6.1&color=blue&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/web-sdk-v0.4.14">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.4.14&color=blue&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-end -->
|
||||
<br/>
|
||||
<a href="https://www.repostatus.org/#wip">
|
||||
<img alt="Project Status" src="https://www.repostatus.org/badges/latest/wip.svg" />
|
||||
</a>
|
||||
<a href="https://open-feature.github.io/js-sdk/modules/_openfeature_web_sdk.html">
|
||||
<img alt="API Reference" src="https://img.shields.io/badge/reference-teal?logo=javascript&logoColor=white" />
|
||||
</a>
|
||||
|
@ -36,7 +39,7 @@
|
|||
</p>
|
||||
<!-- x-hide-in-docs-start -->
|
||||
|
||||
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution.
|
||||
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.
|
||||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
|
||||
|
@ -44,7 +47,7 @@
|
|||
|
||||
### Requirements
|
||||
|
||||
- ES2015-compatible web browser (Chrome, Edge, Firefox, etc)
|
||||
- ES2022-compatible web browser (Chrome, Edge, Firefox, etc)
|
||||
|
||||
### Install
|
||||
|
||||
|
@ -54,9 +57,6 @@
|
|||
npm install --save @openfeature/web-sdk
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> This SDK is designed to run in the browser. If you're interested in server support, check out the [Node.js SDK](https://openfeature.dev/docs/reference/technologies/server/javascript/).
|
||||
|
||||
#### yarn
|
||||
|
||||
```sh
|
||||
|
@ -64,22 +64,13 @@ npm install --save @openfeature/web-sdk
|
|||
yarn add @openfeature/web-sdk @openfeature/core
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> `@openfeature/core` contains common components used by all OpenFeature JavaScript implementations.
|
||||
> Every SDK version has a requirement on a single, specific version of this dependency.
|
||||
> For more information, and similar implications on libraries developed with OpenFeature see [considerations when extending](#considerations).
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { OpenFeature } from '@openfeature/web-sdk';
|
||||
|
||||
// Register your feature flag provider
|
||||
try {
|
||||
await OpenFeature.setProviderAndWait(new YourProviderOfChoice());
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize provider:', error);
|
||||
}
|
||||
await OpenFeature.setProviderAndWait(new YourProviderOfChoice());
|
||||
|
||||
// create a new client
|
||||
const client = OpenFeature.getClient();
|
||||
|
@ -106,7 +97,6 @@ See [here](https://open-feature.github.io/js-sdk/modules/_openfeature_web_sdk.ht
|
|||
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
|
||||
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
|
||||
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|
||||
| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations, particularly for A/B testing. |
|
||||
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|
||||
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
|
||||
|
||||
|
@ -125,12 +115,8 @@ Once you've added a provider as a dependency, it can be registered with OpenFeat
|
|||
To register a provider and ensure it is ready before further actions are taken, you can use the `setProviderAndWait` method as shown below:
|
||||
|
||||
```ts
|
||||
try {
|
||||
await OpenFeature.setProviderAndWait(new MyProvider());
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize provider:', error);
|
||||
}
|
||||
```
|
||||
await OpenFeature.setProviderAndWait(new MyProvider());
|
||||
```
|
||||
|
||||
#### Synchronous
|
||||
|
||||
|
@ -167,20 +153,13 @@ Sometimes, the value of a flag must consider some dynamic criteria about the app
|
|||
In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
|
||||
If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
|
||||
|
||||
```ts
|
||||
// Sets global context during provider registration
|
||||
await OpenFeature.setProvider(new MyProvider(), { origin: document.location.host });
|
||||
```
|
||||
|
||||
Change context after the provider has been registered using `setContext`.
|
||||
|
||||
```ts
|
||||
// Set a value to the global context
|
||||
await OpenFeature.setContext({ targetingKey: localStorage.getItem("targetingKey") });
|
||||
await OpenFeature.setContext({ origin: document.location.host });
|
||||
```
|
||||
|
||||
Context is global and setting it is `async`.
|
||||
Providers may implement an `onContextChange` method that receives the old and newer contexts.
|
||||
Providers may implement an `onContextChanged` method that receives the old and newer contexts.
|
||||
Given a context change, providers can use this method internally to detect if the flag values cached on the client are still valid.
|
||||
If needed, a request will be made to the provider with the new context in order to get the correct flag values.
|
||||
|
||||
|
@ -249,24 +228,6 @@ const domainScopedClient = OpenFeature.getClient("my-domain");
|
|||
Domains can be defined on a provider during registration.
|
||||
For more details, please refer to the [providers](#providers) section.
|
||||
|
||||
#### Manage evaluation context for domains
|
||||
|
||||
By default, domain-scoped clients use the global context.
|
||||
This can be overridden by explicitly setting context when registering the provider or by referencing the domain when updating context:
|
||||
|
||||
```ts
|
||||
OpenFeature.setProvider("my-domain", new NewCachedProvider(), { targetingKey: localStorage.getItem("targetingKey") });
|
||||
```
|
||||
|
||||
To change context after the provider has been registered, use `setContext` with a domain:
|
||||
|
||||
```ts
|
||||
await OpenFeature.setContext("my-domain", { targetingKey: localStorage.getItem("targetingKey") })
|
||||
```
|
||||
|
||||
Once a domain's context has been defined, it will override the global context for all clients bound to the domain.
|
||||
Context can be cleared for a domain by calling `OpenFeature.clearContext("my-domain")` or `OpenFeature.clearContexts()` to reset all context.
|
||||
|
||||
### Eventing
|
||||
|
||||
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
|
||||
|
@ -290,21 +251,6 @@ client.addHandler(ProviderEvents.Error, (eventDetails) => {
|
|||
});
|
||||
```
|
||||
|
||||
### Tracking
|
||||
|
||||
The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations.
|
||||
This is essential for robust experimentation powered by feature flags.
|
||||
For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a [hook](#hooks) or [provider](#providers) can be associated with telemetry reported in the client's `track` function.
|
||||
|
||||
```ts
|
||||
// flag is evaluated
|
||||
client.getBooleanValue('new-feature', false);
|
||||
|
||||
// new feature is used and track function is called recording the usage
|
||||
useNewFeature();
|
||||
client.track('new-feature-used');
|
||||
```
|
||||
|
||||
### Shutdown
|
||||
|
||||
The OpenFeature API provides a close function to perform a cleanup of all registered providers.
|
||||
|
@ -363,7 +309,7 @@ class MyProvider implements Provider {
|
|||
}
|
||||
|
||||
// implement with "new OpenFeatureEventEmitter()", and use "emit()" to emit events
|
||||
events?: ProviderEventEmitter<AnyProviderEvent> | undefined;
|
||||
events?: ProviderEventEmitter<AnyProviderEvent> | undefined;
|
||||
|
||||
initialize?(context?: EvaluationContext | undefined): Promise<void> {
|
||||
// code to initialize your provider
|
||||
|
@ -387,17 +333,9 @@ import type { Hook, HookContext, EvaluationDetails, FlagValue } from "@openfeatu
|
|||
|
||||
export class MyHook implements Hook {
|
||||
after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>) {
|
||||
// code that runs after flag values are successfully resolved from the provider
|
||||
// code that runs when there's an error during a flag evaluation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
|
||||
|
||||
### Considerations
|
||||
|
||||
When developing a library based on OpenFeature components, it's important to list the `@openfeature/web-sdk` as a `peerDependency` of your package.
|
||||
This is a general best-practice when developing JavaScript libraries that have dependencies in common with their consuming application.
|
||||
Failing to do this can result in multiple copies of the OpenFeature SDK in the consumer, which can lead to type errors, and broken singleton behavior.
|
||||
The `@openfeature/core` package itself follows this pattern: the `@openfeature/web-sdk` has a peer dependency on `@openfeature/core`, and uses whatever copy of that module the consumer has installed (note that NPM installs peers automatically unless `--legacy-peer-deps` is set, while yarn does not, and PNPM does so based on its configuration).
|
||||
When developing such libraries, it's NOT necessary to add a `peerDependency` on `@openfeature/core`, since the `@openfeature/web-sdk` establishes that dependency itself transitively.
|
|
@ -1,17 +1,16 @@
|
|||
import type {
|
||||
import {
|
||||
EvaluationContext,
|
||||
EvaluationDetails,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
ResolutionDetails} from '@openfeature/core';
|
||||
import {
|
||||
ResolutionDetails,
|
||||
StandardResolutionReasons,
|
||||
} from '@openfeature/core';
|
||||
import { defineFeature, loadFeature } from 'jest-cucumber';
|
||||
import { InMemoryProvider, OpenFeature } from '../../src';
|
||||
import flagConfiguration from './flags-config';
|
||||
// load the feature file.
|
||||
const feature = loadFeature('packages/web/e2e/features/evaluation.feature');
|
||||
const feature = loadFeature('packages/client/e2e/features/evaluation.feature');
|
||||
|
||||
// get a client (flagd provider registered in setup)
|
||||
const client = OpenFeature.getClient();
|
|
@ -1,10 +1,8 @@
|
|||
{
|
||||
"name": "@openfeature/web-sdk",
|
||||
"version": "1.6.1",
|
||||
"version": "0.4.14",
|
||||
"description": "OpenFeature SDK for Web",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"unpkg": "dist/global/index.min.js",
|
||||
"jsdelivr": "dist/global/index.min.js",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
|
@ -18,18 +16,16 @@
|
|||
"scripts": {
|
||||
"test": "jest --verbose",
|
||||
"lint": "eslint ./",
|
||||
"lint:fix": "eslint ./ --fix",
|
||||
"clean": "shx rm -rf ./dist",
|
||||
"build:web-esm": "esbuild src/index.ts --bundle --external:@openfeature/core --sourcemap --target=es2015 --platform=browser --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:web-cjs": "esbuild src/index.ts --bundle --external:@openfeature/core --sourcemap --target=es2015 --platform=browser --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:web-global": "esbuild src/index.ts --bundle --sourcemap --target=es2015 --platform=browser --format=iife --outfile=./dist/global/index.js --global-name=OpenFeature --analyze",
|
||||
"build:web-global:min": "esbuild src/index.ts --bundle --sourcemap --target=es2015 --platform=browser --format=iife --outfile=./dist/global/index.min.js --global-name=OpenFeature --minify --analyze",
|
||||
"build:web-esm": "esbuild src/index.ts --bundle --external:@openfeature/core --sourcemap --target=es2022 --platform=browser --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:web-cjs": "esbuild src/index.ts --bundle --external:@openfeature/core --sourcemap --target=es2022 --platform=browser --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:rollup-types": "rollup -c ../../rollup.config.mjs",
|
||||
"build": "npm run clean && npm run build:web-esm && npm run build:web-cjs && npm run build:web-global && npm run build:web-global:min && npm run build:rollup-types",
|
||||
"build": "npm run clean && npm run build:web-esm && npm run build:web-cjs && npm run build:rollup-types",
|
||||
"postbuild": "shx cp ./../../package.esm.json ./dist/esm/package.json",
|
||||
"current-version": "echo $npm_package_version",
|
||||
"prepack": "shx cp ./../../LICENSE ./LICENSE",
|
||||
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi"
|
||||
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
|
||||
"update-core-peer": "npm install --save-peer --save-exact @openfeature/core@$OPENFEATURE_CORE_VERSION"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -50,9 +46,9 @@
|
|||
},
|
||||
"homepage": "https://github.com/open-feature/js-sdk#readme",
|
||||
"peerDependencies": {
|
||||
"@openfeature/core": "^1.9.0"
|
||||
"@openfeature/core": "0.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openfeature/core": "^1.9.0"
|
||||
"@openfeature/core": "0.0.27"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { ClientMetadata, EvaluationLifeCycle, Eventing, ManageLogger } from '@openfeature/core';
|
||||
import { Features } from '../evaluation';
|
||||
import { ProviderStatus } from '../provider';
|
||||
import { ProviderEvents } from '../events';
|
||||
|
||||
export interface Client extends EvaluationLifeCycle<Client>, Features, ManageLogger<Client>, Eventing<ProviderEvents> {
|
||||
readonly metadata: ClientMetadata;
|
||||
/**
|
||||
* Returns the status of the associated provider.
|
||||
*/
|
||||
readonly providerStatus: ProviderStatus;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './client';
|
||||
export * from './open-feature-client';
|
|
@ -1,5 +1,6 @@
|
|||
import type {
|
||||
import {
|
||||
ClientMetadata,
|
||||
ErrorCode,
|
||||
EvaluationContext,
|
||||
EvaluationDetails,
|
||||
EventHandler,
|
||||
|
@ -8,29 +9,21 @@ import type {
|
|||
HookContext,
|
||||
JsonValue,
|
||||
Logger,
|
||||
TrackingEventDetails,
|
||||
OpenFeatureError,
|
||||
FlagMetadata,
|
||||
ResolutionDetails,
|
||||
EventOptions,
|
||||
} from '@openfeature/core';
|
||||
import {
|
||||
ErrorCode,
|
||||
ProviderFatalError,
|
||||
ProviderNotReadyError,
|
||||
ResolutionDetails,
|
||||
SafeLogger,
|
||||
StandardResolutionReasons,
|
||||
instantiateErrorByErrorCode,
|
||||
statusMatchesEvent,
|
||||
MapHookData,
|
||||
statusMatchesEvent
|
||||
} from '@openfeature/core';
|
||||
import type { FlagEvaluationOptions } from '../../evaluation';
|
||||
import type { ProviderEvents } from '../../events';
|
||||
import type { InternalEventEmitter } from '../../events/internal/internal-event-emitter';
|
||||
import type { Hook } from '../../hooks';
|
||||
import type { Provider } from '../../provider';
|
||||
import { ProviderStatus } from '../../provider';
|
||||
import type { Client } from './../client';
|
||||
import { FlagEvaluationOptions } from '../evaluation';
|
||||
import { ProviderEvents } from '../events';
|
||||
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
|
||||
import { Hook } from '../hooks';
|
||||
import { OpenFeature } from '../open-feature';
|
||||
import { Provider, ProviderStatus } from '../provider';
|
||||
import { Client } from './client';
|
||||
|
||||
type OpenFeatureClientOptions = {
|
||||
/**
|
||||
|
@ -41,11 +34,6 @@ type OpenFeatureClientOptions = {
|
|||
version?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This implementation of the {@link Client} is meant to only be instantiated by the SDK.
|
||||
* It should not be used outside the SDK and so should not be exported.
|
||||
* @internal
|
||||
*/
|
||||
export class OpenFeatureClient implements Client {
|
||||
private _hooks: Hook[] = [];
|
||||
private _clientLogger?: Logger;
|
||||
|
@ -56,8 +44,6 @@ export class OpenFeatureClient implements Client {
|
|||
private readonly providerAccessor: () => Provider,
|
||||
private readonly providerStatusAccessor: () => ProviderStatus,
|
||||
private readonly emitterAccessor: () => InternalEventEmitter,
|
||||
private readonly apiContextAccessor: (domain?: string) => EvaluationContext,
|
||||
private readonly apiHooksAccessor: () => Hook[],
|
||||
private readonly globalLogger: () => Logger,
|
||||
private readonly options: OpenFeatureClientOptions,
|
||||
) {}
|
||||
|
@ -76,7 +62,7 @@ export class OpenFeatureClient implements Client {
|
|||
return this.providerStatusAccessor();
|
||||
}
|
||||
|
||||
addHandler(eventType: ProviderEvents, handler: EventHandler, options: EventOptions): void {
|
||||
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
|
||||
this.emitterAccessor().addHandler(eventType, handler);
|
||||
const shouldRunNow = statusMatchesEvent(eventType, this.providerStatus);
|
||||
|
||||
|
@ -92,12 +78,6 @@ export class OpenFeatureClient implements Client {
|
|||
this._logger?.error('Error running event handler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.signal && typeof options.signal.addEventListener === 'function') {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
this.removeHandler(eventType, handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeHandler(notificationType: ProviderEvents, handler: EventHandler): void {
|
||||
|
@ -193,24 +173,6 @@ export class OpenFeatureClient implements Client {
|
|||
return this.evaluate<T>(flagKey, this._provider.resolveObjectEvaluation, defaultValue, 'object', options);
|
||||
}
|
||||
|
||||
track(occurrenceKey: string, occurrenceDetails: TrackingEventDetails = {}): void {
|
||||
try {
|
||||
this.shortCircuitIfNotReady();
|
||||
|
||||
if (typeof this._provider.track === 'function') {
|
||||
// copy and freeze the context
|
||||
const frozenContext = Object.freeze({
|
||||
...this.apiContextAccessor(this?.options?.domain),
|
||||
});
|
||||
return this._provider.track?.(occurrenceKey, frozenContext, occurrenceDetails);
|
||||
} else {
|
||||
this._logger.debug('Provider does not support the track function; will no-op.');
|
||||
}
|
||||
} catch (err) {
|
||||
this._logger.debug('Error recording tracking event.', err);
|
||||
}
|
||||
}
|
||||
|
||||
private evaluate<T extends FlagValue>(
|
||||
flagKey: string,
|
||||
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger) => ResolutionDetails<T>,
|
||||
|
@ -221,7 +183,7 @@ export class OpenFeatureClient implements Client {
|
|||
// merge global, client, and evaluation context
|
||||
|
||||
const allHooks = [
|
||||
...this.apiHooksAccessor(),
|
||||
...OpenFeature.getHooks(),
|
||||
...this.getHooks(),
|
||||
...(options.hooks || []),
|
||||
...(this._provider.hooks || []),
|
||||
|
@ -229,85 +191,87 @@ export class OpenFeatureClient implements Client {
|
|||
const allHooksReversed = [...allHooks].reverse();
|
||||
|
||||
const context = {
|
||||
...this.apiContextAccessor(this?.options?.domain),
|
||||
...OpenFeature.getContext(this?.options?.domain),
|
||||
};
|
||||
|
||||
// Create hook context instances for each hook (stable object references for the entire evaluation)
|
||||
// This ensures hooks can use WeakMaps with hookContext as keys across lifecycle methods
|
||||
// NOTE: Uses the reversed order to reduce the number of times we have to calculate the index.
|
||||
const hookContexts = allHooksReversed.map<HookContext>(() =>
|
||||
Object.freeze({
|
||||
flagKey,
|
||||
defaultValue,
|
||||
flagValueType: flagType,
|
||||
clientMetadata: this.metadata,
|
||||
providerMetadata: this._provider.metadata,
|
||||
context,
|
||||
logger: this._logger,
|
||||
hookData: new MapHookData(),
|
||||
}),
|
||||
);
|
||||
|
||||
let evaluationDetails: EvaluationDetails<T>;
|
||||
// this reference cannot change during the course of evaluation
|
||||
// it may be used as a key in WeakMaps
|
||||
const hookContext: Readonly<HookContext> = {
|
||||
flagKey,
|
||||
defaultValue,
|
||||
flagValueType: flagType,
|
||||
clientMetadata: this.metadata,
|
||||
providerMetadata: OpenFeature.providerMetadata,
|
||||
context,
|
||||
logger: this._logger,
|
||||
};
|
||||
|
||||
try {
|
||||
this.beforeHooks(allHooks, hookContexts, options);
|
||||
|
||||
this.shortCircuitIfNotReady();
|
||||
this.beforeHooks(allHooks, hookContext, options);
|
||||
|
||||
// short circuit evaluation entirely if provider is in a bad state
|
||||
if (this.providerStatus === ProviderStatus.NOT_READY) {
|
||||
throw new ProviderNotReadyError('provider has not yet initialized');
|
||||
} else if (this.providerStatus === ProviderStatus.FATAL) {
|
||||
throw new ProviderFatalError('provider is in an irrecoverable error state');
|
||||
}
|
||||
|
||||
// run the referenced resolver, binding the provider.
|
||||
const resolution = resolver.call(this._provider, flagKey, defaultValue, context, this._logger);
|
||||
|
||||
const resolutionDetails = {
|
||||
const evaluationDetails = {
|
||||
...resolution,
|
||||
flagMetadata: Object.freeze(resolution.flagMetadata ?? {}),
|
||||
flagKey,
|
||||
};
|
||||
|
||||
if (resolutionDetails.errorCode) {
|
||||
const err = instantiateErrorByErrorCode(resolutionDetails.errorCode, resolutionDetails.errorMessage);
|
||||
this.errorHooks(allHooksReversed, hookContexts, err, options);
|
||||
evaluationDetails = this.getErrorEvaluationDetails(flagKey, defaultValue, err, resolutionDetails.flagMetadata);
|
||||
} else {
|
||||
this.afterHooks(allHooksReversed, hookContexts, resolutionDetails, options);
|
||||
evaluationDetails = resolutionDetails;
|
||||
}
|
||||
this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
||||
|
||||
return evaluationDetails;
|
||||
} catch (err: unknown) {
|
||||
this.errorHooks(allHooksReversed, hookContexts, err, options);
|
||||
evaluationDetails = this.getErrorEvaluationDetails(flagKey, defaultValue, err);
|
||||
const errorMessage: string = (err as Error)?.message;
|
||||
const errorCode: ErrorCode = (err as OpenFeatureError)?.code || ErrorCode.GENERAL;
|
||||
|
||||
this.errorHooks(allHooksReversed, hookContext, err, options);
|
||||
|
||||
return {
|
||||
errorCode,
|
||||
errorMessage,
|
||||
value: defaultValue,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
flagMetadata: Object.freeze({}),
|
||||
flagKey,
|
||||
};
|
||||
} finally {
|
||||
this.finallyHooks(allHooksReversed, hookContext, options);
|
||||
}
|
||||
this.finallyHooks(allHooksReversed, hookContexts, evaluationDetails, options);
|
||||
return evaluationDetails;
|
||||
}
|
||||
|
||||
private beforeHooks(hooks: Hook[], hookContexts: HookContext[], options: FlagEvaluationOptions) {
|
||||
for (const [index, hook] of hooks.entries()) {
|
||||
const hookContextIndex = hooks.length - 1 - index; // reverse index for before hooks
|
||||
const hookContext = hookContexts[hookContextIndex];
|
||||
Object.freeze(hookContext);
|
||||
Object.freeze(hookContext.context);
|
||||
private beforeHooks(hooks: Hook[], hookContext: HookContext, options: FlagEvaluationOptions) {
|
||||
Object.freeze(hookContext);
|
||||
Object.freeze(hookContext.context);
|
||||
|
||||
for (const hook of hooks) {
|
||||
hook?.before?.(hookContext, Object.freeze(options.hookHints));
|
||||
}
|
||||
}
|
||||
|
||||
private afterHooks(
|
||||
hooks: Hook[],
|
||||
hookContexts: HookContext[],
|
||||
hookContext: HookContext,
|
||||
evaluationDetails: EvaluationDetails<FlagValue>,
|
||||
options: FlagEvaluationOptions,
|
||||
) {
|
||||
// run "after" hooks sequentially
|
||||
for (const [index, hook] of hooks.entries()) {
|
||||
const hookContext = hookContexts[index];
|
||||
for (const hook of hooks) {
|
||||
hook?.after?.(hookContext, evaluationDetails, options.hookHints);
|
||||
}
|
||||
}
|
||||
|
||||
private errorHooks(hooks: Hook[], hookContexts: HookContext[], err: unknown, options: FlagEvaluationOptions) {
|
||||
private errorHooks(hooks: Hook[], hookContext: HookContext, err: unknown, options: FlagEvaluationOptions) {
|
||||
// run "error" hooks sequentially
|
||||
for (const [index, hook] of hooks.entries()) {
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
const hookContext = hookContexts[index];
|
||||
hook?.error?.(hookContext, err, options.hookHints);
|
||||
} catch (err) {
|
||||
this._logger.error(`Unhandled error during 'error' hook: ${err}`);
|
||||
|
@ -319,17 +283,11 @@ export class OpenFeatureClient implements Client {
|
|||
}
|
||||
}
|
||||
|
||||
private finallyHooks(
|
||||
hooks: Hook[],
|
||||
hookContexts: HookContext[],
|
||||
evaluationDetails: EvaluationDetails<FlagValue>,
|
||||
options: FlagEvaluationOptions,
|
||||
) {
|
||||
private finallyHooks(hooks: Hook[], hookContext: HookContext, options: FlagEvaluationOptions) {
|
||||
// run "finally" hooks sequentially
|
||||
for (const [index, hook] of hooks.entries()) {
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
const hookContext = hookContexts[index];
|
||||
hook?.finally?.(hookContext, evaluationDetails, options.hookHints);
|
||||
hook?.finally?.(hookContext, options.hookHints);
|
||||
} catch (err) {
|
||||
this._logger.error(`Unhandled error during 'finally' hook: ${err}`);
|
||||
if (err instanceof Error) {
|
||||
|
@ -347,32 +305,4 @@ export class OpenFeatureClient implements Client {
|
|||
private get _logger() {
|
||||
return this._clientLogger || this.globalLogger();
|
||||
}
|
||||
|
||||
private shortCircuitIfNotReady() {
|
||||
// short circuit evaluation entirely if provider is in a bad state
|
||||
if (this.providerStatus === ProviderStatus.NOT_READY) {
|
||||
throw new ProviderNotReadyError('provider has not yet initialized');
|
||||
} else if (this.providerStatus === ProviderStatus.FATAL) {
|
||||
throw new ProviderFatalError('provider is in an irrecoverable error state');
|
||||
}
|
||||
}
|
||||
|
||||
private getErrorEvaluationDetails<T extends FlagValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
err: unknown,
|
||||
flagMetadata: FlagMetadata = {},
|
||||
): EvaluationDetails<T> {
|
||||
const errorMessage: string = (err as Error)?.message;
|
||||
const errorCode: ErrorCode = (err as OpenFeatureError)?.code || ErrorCode.GENERAL;
|
||||
|
||||
return {
|
||||
errorCode,
|
||||
errorMessage,
|
||||
value: defaultValue,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
flagMetadata: Object.freeze(flagMetadata),
|
||||
flagKey,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { EvaluationDetails, BaseHook, HookHints, JsonValue } from '@openfeature/core';
|
||||
import { EvaluationDetails, BaseHook, HookHints, JsonValue } from '@openfeature/core';
|
||||
|
||||
export interface FlagEvaluationOptions {
|
||||
hooks?: BaseHook[];
|
|
@ -1,6 +1,5 @@
|
|||
import type { CommonEventDetails} from '@openfeature/core';
|
||||
import { GenericEventEmitter } from '@openfeature/core';
|
||||
import type { ProviderEvents } from '../events';
|
||||
import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
|
||||
import { ProviderEvents } from '../events';
|
||||
|
||||
/**
|
||||
* The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
|
|
@ -1,7 +1,6 @@
|
|||
import { GenericEventEmitter } from '@openfeature/core';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import type { ProviderEmittableEvents } from './events';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProviderEmittableEvents } from './events';
|
||||
/**
|
||||
* The OpenFeatureEventEmitter can be used by provider developers to emit
|
||||
* events at various parts of the provider lifecycle.
|
||||
|
@ -10,9 +9,12 @@ import type { ProviderEmittableEvents } from './events';
|
|||
* the result of the initialize method.
|
||||
*/
|
||||
export class OpenFeatureEventEmitter extends GenericEventEmitter<ProviderEmittableEvents> {
|
||||
protected readonly eventEmitter = new EventEmitter();
|
||||
protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.eventEmitter.on('error', (err) => {
|
||||
this._logger?.error('Error running event handler:', err);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { BaseHook, FlagValue } from '@openfeature/core';
|
||||
|
||||
export type Hook = BaseHook<FlagValue, void, void>;
|
|
@ -4,5 +4,4 @@ export * from './evaluation';
|
|||
export * from './open-feature';
|
||||
export * from './events';
|
||||
export * from './hooks';
|
||||
export * from './tracking';
|
||||
export * from '@openfeature/core';
|
|
@ -1,20 +1,17 @@
|
|||
import type {
|
||||
import {
|
||||
ClientProviderStatus,
|
||||
EvaluationContext,
|
||||
GenericEventEmitter,
|
||||
ManageContext} from '@openfeature/core';
|
||||
import {
|
||||
ManageContext,
|
||||
OpenFeatureCommonAPI,
|
||||
ProviderWrapper,
|
||||
objectOrUndefined,
|
||||
stringOrUndefined,
|
||||
} from '@openfeature/core';
|
||||
import type { Client } from './client';
|
||||
import { OpenFeatureClient } from './client/internal/open-feature-client';
|
||||
import { Client, OpenFeatureClient } from './client';
|
||||
import { OpenFeatureEventEmitter, ProviderEvents } from './events';
|
||||
import type { Hook } from './hooks';
|
||||
import type { Provider} from './provider';
|
||||
import { NOOP_PROVIDER, ProviderStatus } from './provider';
|
||||
import { Hook } from './hooks';
|
||||
import { NOOP_PROVIDER, Provider, ProviderStatus } from './provider';
|
||||
|
||||
// use a symbol as a key for the global singleton
|
||||
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
|
||||
|
@ -29,17 +26,10 @@ type DomainRecord = {
|
|||
|
||||
const _globalThis = globalThis as OpenFeatureGlobal;
|
||||
|
||||
export class OpenFeatureAPI
|
||||
extends OpenFeatureCommonAPI<ClientProviderStatus, Provider, Hook>
|
||||
implements ManageContext<Promise<void>>
|
||||
{
|
||||
export class OpenFeatureAPI extends OpenFeatureCommonAPI<ClientProviderStatus, Provider, Hook> implements ManageContext<Promise<void>> {
|
||||
protected _statusEnumType: typeof ProviderStatus = ProviderStatus;
|
||||
protected _apiEmitter: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter();
|
||||
protected _defaultProvider: ProviderWrapper<Provider, ClientProviderStatus> = new ProviderWrapper(
|
||||
NOOP_PROVIDER,
|
||||
ProviderStatus.NOT_READY,
|
||||
this._statusEnumType,
|
||||
);
|
||||
protected _defaultProvider: ProviderWrapper<Provider, ClientProviderStatus> = new ProviderWrapper(NOOP_PROVIDER, ProviderStatus.NOT_READY, this._statusEnumType);
|
||||
protected _domainScopedProviders: Map<string, ProviderWrapper<Provider, ClientProviderStatus>> = new Map();
|
||||
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
|
||||
|
||||
|
@ -71,161 +61,6 @@ export class OpenFeatureAPI
|
|||
return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
|
||||
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
|
||||
* Setting a provider supersedes the current provider used in new and existing unbound clients.
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the provider throws an exception during initialization.
|
||||
*/
|
||||
setProviderAndWait(provider: Provider): Promise<void>;
|
||||
/**
|
||||
* Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
|
||||
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
|
||||
* Setting a provider supersedes the current provider used in new and existing unbound clients.
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the provider throws an exception during initialization.
|
||||
*/
|
||||
setProviderAndWait(provider: Provider, context: EvaluationContext): Promise<void>;
|
||||
/**
|
||||
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
|
||||
* A promise is returned that resolves when the provider is ready.
|
||||
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
|
||||
* @param {string} domain The name to identify the client
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the provider throws an exception during initialization.
|
||||
*/
|
||||
setProviderAndWait(domain: string, provider: Provider): Promise<void>;
|
||||
/**
|
||||
* Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
|
||||
* A promise is returned that resolves when the provider is ready.
|
||||
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
|
||||
* @param {string} domain The name to identify the client
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @param {EvaluationContext} context The evaluation context to use for flag evaluations.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the provider throws an exception during initialization.
|
||||
*/
|
||||
setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext): Promise<void>;
|
||||
async setProviderAndWait(
|
||||
clientOrProvider?: string | Provider,
|
||||
providerContextOrUndefined?: Provider | EvaluationContext,
|
||||
contextOrUndefined?: EvaluationContext,
|
||||
): Promise<void> {
|
||||
const domain = stringOrUndefined(clientOrProvider);
|
||||
const provider = domain
|
||||
? objectOrUndefined<Provider>(providerContextOrUndefined)
|
||||
: objectOrUndefined<Provider>(clientOrProvider);
|
||||
const context = domain
|
||||
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
|
||||
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
|
||||
|
||||
if (context) {
|
||||
// synonymously setting context prior to provider initialization.
|
||||
// No context change event will be emitted.
|
||||
if (domain) {
|
||||
this._domainScopedContext.set(domain, context);
|
||||
} else {
|
||||
this._context = context;
|
||||
}
|
||||
}
|
||||
|
||||
await this.setAwaitableProvider(domain, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default provider for flag evaluations.
|
||||
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
|
||||
* Setting a provider supersedes the current provider used in new and existing unbound clients.
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @returns {this} OpenFeature API
|
||||
*/
|
||||
setProvider(provider: Provider): this;
|
||||
/**
|
||||
* Sets the default provider and evaluation context for flag evaluations.
|
||||
* This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
|
||||
* Setting a provider supersedes the current provider used in new and existing unbound clients.
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
|
||||
* @returns {this} OpenFeature API
|
||||
*/
|
||||
setProvider(provider: Provider, context: EvaluationContext): this;
|
||||
/**
|
||||
* Sets the provider for flag evaluations of providers with the given name.
|
||||
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
|
||||
* @param {string} domain The name to identify the client
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @returns {this} OpenFeature API
|
||||
*/
|
||||
setProvider(domain: string, provider: Provider): this;
|
||||
/**
|
||||
* Sets the provider and evaluation context flag evaluations of providers with the given name.
|
||||
* Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
|
||||
* @param {string} domain The name to identify the client
|
||||
* @param {Provider} provider The provider responsible for flag evaluations.
|
||||
* @param context {EvaluationContext} The evaluation context to use for flag evaluations.
|
||||
* @returns {this} OpenFeature API
|
||||
*/
|
||||
setProvider(domain: string, provider: Provider, context: EvaluationContext): this;
|
||||
setProvider(
|
||||
domainOrProvider?: string | Provider,
|
||||
providerContextOrUndefined?: Provider | EvaluationContext,
|
||||
contextOrUndefined?: EvaluationContext,
|
||||
): this {
|
||||
const domain = stringOrUndefined(domainOrProvider);
|
||||
const provider = domain
|
||||
? objectOrUndefined<Provider>(providerContextOrUndefined)
|
||||
: objectOrUndefined<Provider>(domainOrProvider);
|
||||
const context = domain
|
||||
? objectOrUndefined<EvaluationContext>(contextOrUndefined)
|
||||
: objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
|
||||
|
||||
if (context) {
|
||||
// synonymously setting context prior to provider initialization.
|
||||
// No context change event will be emitted.
|
||||
if (domain) {
|
||||
this._domainScopedContext.set(domain, context);
|
||||
} else {
|
||||
this._context = context;
|
||||
}
|
||||
}
|
||||
|
||||
const maybePromise = this.setAwaitableProvider(domain, provider);
|
||||
|
||||
// The setProvider method doesn't return a promise so we need to catch and
|
||||
// log any errors that occur during provider initialization to avoid having
|
||||
// an unhandled promise rejection.
|
||||
Promise.resolve(maybePromise).catch((err) => {
|
||||
this._logger.error('Error during provider initialization:', err);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default provider.
|
||||
*
|
||||
* Note that it isn't recommended to interact with the provider directly, but rather through
|
||||
* an OpenFeature client.
|
||||
* @returns {Provider} Default Provider
|
||||
*/
|
||||
getProvider(): Provider;
|
||||
/**
|
||||
* Get the provider bound to the specified domain.
|
||||
*
|
||||
* Note that it isn't recommended to interact with the provider directly, but rather through
|
||||
* an OpenFeature client.
|
||||
* @param {string} domain An identifier which logically binds clients with providers
|
||||
* @returns {Provider} Domain-scoped provider
|
||||
*/
|
||||
getProvider(domain?: string): Provider;
|
||||
getProvider(domain?: string): Provider {
|
||||
return this.getProviderForClient(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the evaluation context globally.
|
||||
* This will be used by all providers that have not bound to a domain.
|
||||
|
@ -276,7 +111,9 @@ export class OpenFeatureAPI
|
|||
...unboundProviders,
|
||||
];
|
||||
await Promise.all(
|
||||
allDomainRecords.map((dm) => this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context)),
|
||||
allDomainRecords.map((dm) =>
|
||||
this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -292,7 +129,7 @@ export class OpenFeatureAPI
|
|||
* @param {string} domain An identifier which logically binds clients with providers
|
||||
* @returns {EvaluationContext} Evaluation context
|
||||
*/
|
||||
getContext(domain?: string | undefined): EvaluationContext;
|
||||
getContext(domain?: string): EvaluationContext;
|
||||
getContext(domainOrUndefined?: string): EvaluationContext {
|
||||
const domain = stringOrUndefined(domainOrUndefined);
|
||||
if (domain) {
|
||||
|
@ -346,9 +183,9 @@ export class OpenFeatureAPI
|
|||
}
|
||||
|
||||
/**
|
||||
* A factory function for creating new domain-scoped OpenFeature clients. Clients
|
||||
* can contain their own state (e.g. logger, hook, context). Multiple domains
|
||||
* can be used to segment feature flag configuration.
|
||||
* A factory function for creating new named OpenFeature clients. Clients can contain
|
||||
* their own state (e.g. logger, hook, context). Multiple clients can be used
|
||||
* to segment feature flag configuration.
|
||||
*
|
||||
* If there is already a provider bound to this name via {@link this.setProvider setProvider}, this provider will be used.
|
||||
* Otherwise, the default provider is used until a provider is assigned to that name.
|
||||
|
@ -363,8 +200,6 @@ export class OpenFeatureAPI
|
|||
() => this.getProviderForClient(domain),
|
||||
() => this.getProviderStatus(domain),
|
||||
() => this.buildAndCacheEventEmitterForClient(domain),
|
||||
(domain?: string) => this.getContext(domain),
|
||||
() => this.getHooks(),
|
||||
() => this._logger,
|
||||
{ domain, version },
|
||||
);
|
||||
|
@ -387,23 +222,17 @@ export class OpenFeatureAPI
|
|||
): Promise<void> {
|
||||
// this should always be set according to the typings, but let's be defensive considering JS
|
||||
const providerName = wrapper.provider?.metadata?.name || 'unnamed-provider';
|
||||
|
||||
|
||||
try {
|
||||
if (typeof wrapper.provider.onContextChange === 'function') {
|
||||
const maybePromise = wrapper.provider.onContextChange(oldContext, newContext);
|
||||
|
||||
// only reconcile if the onContextChange method returns a promise
|
||||
if (typeof maybePromise?.then === 'function') {
|
||||
wrapper.incrementPendingContextChanges();
|
||||
wrapper.status = this._statusEnumType.RECONCILING;
|
||||
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
||||
emitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
|
||||
});
|
||||
this._apiEmitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
|
||||
|
||||
await maybePromise;
|
||||
wrapper.decrementPendingContextChanges();
|
||||
}
|
||||
wrapper.incrementPendingContextChanges();
|
||||
wrapper.status = this._statusEnumType.RECONCILING;
|
||||
this.getAssociatedEventEmitters(domain).forEach((emitter) => {
|
||||
emitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
|
||||
});
|
||||
this._apiEmitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
|
||||
await wrapper.provider.onContextChange(oldContext, newContext);
|
||||
wrapper.decrementPendingContextChanges();
|
||||
}
|
||||
// only run the event handlers, and update the state if the onContextChange method succeeded
|
||||
wrapper.status = this._statusEnumType.READY;
|
|
@ -3,7 +3,7 @@
|
|||
* It might cause confusion since these types are not a part of the general API,
|
||||
* but just for the in-memory provider.
|
||||
*/
|
||||
import type { EvaluationContext, JsonValue } from '@openfeature/core';
|
||||
import { EvaluationContext, JsonValue } from '@openfeature/core';
|
||||
|
||||
type Variants<T> = Record<string, T>;
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
import type {
|
||||
import {
|
||||
EvaluationContext,
|
||||
FlagNotFoundError,
|
||||
FlagValueType,
|
||||
GeneralError,
|
||||
JsonValue,
|
||||
Logger,
|
||||
ResolutionDetails} from '@openfeature/core';
|
||||
import {
|
||||
FlagNotFoundError,
|
||||
GeneralError,
|
||||
OpenFeatureError,
|
||||
ResolutionDetails,
|
||||
StandardResolutionReasons,
|
||||
TypeMismatchError,
|
||||
} from '@openfeature/core';
|
||||
import type { Provider } from '../provider';
|
||||
import { Provider } from '../provider';
|
||||
import { OpenFeatureEventEmitter, ProviderEvents } from '../../events';
|
||||
import type { FlagConfiguration, Flag } from './flag-configuration';
|
||||
import { FlagConfiguration, Flag } from './flag-configuration';
|
||||
import { VariantNotFoundError } from './variant-not-found-error';
|
||||
|
||||
/**
|
||||
|
@ -32,16 +31,30 @@ export class InMemoryProvider implements Provider {
|
|||
this._flagConfiguration = { ...flagConfiguration };
|
||||
}
|
||||
|
||||
async initialize(context?: EvaluationContext | undefined): Promise<void> {
|
||||
try {
|
||||
for (const key in this._flagConfiguration) {
|
||||
this.resolveFlagWithReason(key, context);
|
||||
}
|
||||
this._context = context;
|
||||
} catch (err) {
|
||||
throw new Error('initialization failure', { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the configured flags.
|
||||
* @param { FlagConfiguration } flagConfiguration new flag configuration
|
||||
*/
|
||||
async putConfiguration(flagConfiguration: FlagConfiguration) {
|
||||
try {
|
||||
const flagsChanged = Object.entries({...flagConfiguration, ...this._flagConfiguration})
|
||||
.map(([key]) => key);
|
||||
const flagsChanged = Object.entries(flagConfiguration)
|
||||
.filter(([key, value]) => this._flagConfiguration[key] !== value)
|
||||
.map(([key]) => key);
|
||||
|
||||
this._flagConfiguration = { ...flagConfiguration };
|
||||
this._flagConfiguration = { ...flagConfiguration };
|
||||
|
||||
try {
|
||||
await this.initialize(this._context);
|
||||
this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged });
|
||||
} catch (err) {
|
||||
this.events.emit(ProviderEvents.Error);
|
||||
|
@ -85,12 +98,8 @@ export class InMemoryProvider implements Provider {
|
|||
return this.resolveAndCheckFlag<T>(flagKey, defaultValue, context || this._context, logger);
|
||||
}
|
||||
|
||||
private resolveAndCheckFlag<T extends JsonValue | FlagValueType>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
context?: EvaluationContext,
|
||||
logger?: Logger,
|
||||
): ResolutionDetails<T> {
|
||||
private resolveAndCheckFlag<T extends JsonValue | FlagValueType>(flagKey: string,
|
||||
defaultValue: T, context?: EvaluationContext, logger?: Logger): ResolutionDetails<T> {
|
||||
if (!(flagKey in this._flagConfiguration)) {
|
||||
const message = `no flag found with key ${flagKey}`;
|
||||
logger?.debug(message);
|
|
@ -1,5 +1,5 @@
|
|||
import type { JsonValue, ResolutionDetails } from '@openfeature/core';
|
||||
import type { Provider } from './provider';
|
||||
import { JsonValue, ResolutionDetails } from '@openfeature/core';
|
||||
import { Provider } from './provider';
|
||||
|
||||
const REASON_NO_OP = 'No-op';
|
||||
|
|
@ -1,12 +1,5 @@
|
|||
import type {
|
||||
CommonProvider,
|
||||
EvaluationContext,
|
||||
JsonValue,
|
||||
Logger,
|
||||
ResolutionDetails,
|
||||
} from '@openfeature/core';
|
||||
import { ClientProviderStatus } from '@openfeature/core';
|
||||
import type { Hook } from '../hooks';
|
||||
import { ClientProviderStatus, CommonProvider, EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/core';
|
||||
import { Hook } from '../hooks';
|
||||
|
||||
export { ClientProviderStatus as ProviderStatus };
|
||||
|
||||
|
@ -26,18 +19,12 @@ export interface Provider extends CommonProvider<ClientProviderStatus> {
|
|||
readonly hooks?: Hook[];
|
||||
|
||||
/**
|
||||
* A handler function to reconcile changes made to the static context.
|
||||
* A handler function to reconcile changes when the static context.
|
||||
* Called by the SDK when the context is changed.
|
||||
*
|
||||
* Returning a promise will put the provider in the RECONCILING state and
|
||||
* emit the ProviderEvents.Reconciling event.
|
||||
*
|
||||
* Return void will avoid putting the provider in the RECONCILING state and
|
||||
* **not** emit the ProviderEvents.Reconciling event.
|
||||
* @param oldContext
|
||||
* @param newContext
|
||||
*/
|
||||
onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> | void;
|
||||
onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void>;
|
||||
|
||||
/**
|
||||
* Resolve a boolean flag and its evaluation details.
|
|
@ -1,15 +1,20 @@
|
|||
import type { TrackingEventDetails } from '@openfeature/core';
|
||||
import type { Client, EvaluationDetails, JsonArray, JsonObject, JsonValue, Provider, ResolutionDetails } from '../src';
|
||||
import {
|
||||
Client,
|
||||
ErrorCode,
|
||||
EvaluationDetails,
|
||||
FlagNotFoundError,
|
||||
GeneralError,
|
||||
JsonArray,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
OpenFeature,
|
||||
OpenFeatureClient,
|
||||
Provider,
|
||||
ProviderFatalError,
|
||||
ProviderStatus,
|
||||
ResolutionDetails,
|
||||
StandardResolutionReasons,
|
||||
} from '../src';
|
||||
import { OpenFeatureClient } from '../src/client/internal/open-feature-client';
|
||||
|
||||
const BOOLEAN_VALUE = true;
|
||||
const STRING_VALUE = 'val';
|
||||
|
@ -56,10 +61,6 @@ const MOCK_PROVIDER: Provider = {
|
|||
return Promise.resolve(undefined);
|
||||
},
|
||||
|
||||
track: jest.fn((): void => {
|
||||
return;
|
||||
}),
|
||||
|
||||
resolveNumberEvaluation: jest.fn((): ResolutionDetails<number> => {
|
||||
return {
|
||||
value: NUMBER_VALUE,
|
||||
|
@ -129,15 +130,12 @@ describe('OpenFeatureClient', () => {
|
|||
resolveBooleanEvaluation(): ResolutionDetails<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
resolveStringEvaluation(): ResolutionDetails<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
resolveNumberEvaluation(): ResolutionDetails<number> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
@ -238,7 +236,7 @@ describe('OpenFeatureClient', () => {
|
|||
numberFlag,
|
||||
defaultNumberValue,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -250,7 +248,7 @@ describe('OpenFeatureClient', () => {
|
|||
const defaultNumberValue = 4096;
|
||||
const value: MyRestrictedNumber = client.getNumberValue<MyRestrictedNumber>(
|
||||
numberFlag,
|
||||
defaultNumberValue,
|
||||
defaultNumberValue
|
||||
);
|
||||
|
||||
expect(value).toEqual(NUMBER_VALUE);
|
||||
|
@ -258,7 +256,7 @@ describe('OpenFeatureClient', () => {
|
|||
numberFlag,
|
||||
defaultNumberValue,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -335,7 +333,7 @@ describe('OpenFeatureClient', () => {
|
|||
booleanFlag,
|
||||
defaultBooleanValue,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -384,7 +382,7 @@ describe('OpenFeatureClient', () => {
|
|||
// No generic information exists at runtime, but this test has some value in ensuring the generic args still exist in the typings.
|
||||
const client = OpenFeature.getClient();
|
||||
const details: ResolutionDetails<JsonValue> = client.getObjectDetails('flag', { key: 'value' });
|
||||
|
||||
|
||||
expect(details).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@ -397,7 +395,7 @@ describe('OpenFeatureClient', () => {
|
|||
},
|
||||
initialize: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
}
|
||||
} as unknown as Provider;
|
||||
it('status must be READY if init resolves', async () => {
|
||||
await OpenFeature.setProviderAndWait('1.7.1, 1.7.3', initProvider);
|
||||
|
@ -413,7 +411,7 @@ describe('OpenFeatureClient', () => {
|
|||
},
|
||||
initialize: async () => {
|
||||
return Promise.reject(new GeneralError());
|
||||
},
|
||||
}
|
||||
} as unknown as Provider;
|
||||
it('status must be ERROR if init rejects', async () => {
|
||||
await expect(OpenFeature.setProviderAndWait('1.7.4', errorProvider)).rejects.toThrow();
|
||||
|
@ -429,7 +427,7 @@ describe('OpenFeatureClient', () => {
|
|||
},
|
||||
initialize: () => {
|
||||
return Promise.reject(new ProviderFatalError());
|
||||
},
|
||||
}
|
||||
} as unknown as Provider;
|
||||
it('must short circuit and return PROVIDER_FATAL code if provider FATAL', async () => {
|
||||
await expect(OpenFeature.setProviderAndWait('1.7.5, 1.7.6, 1.7.8', fatalProvider)).rejects.toThrow();
|
||||
|
@ -452,7 +450,7 @@ describe('OpenFeatureClient', () => {
|
|||
return new Promise(() => {
|
||||
return; // promise never resolves
|
||||
});
|
||||
},
|
||||
}
|
||||
} as unknown as Provider;
|
||||
it('must short circuit and return PROVIDER_NOT_READY code if provider NOT_READY', async () => {
|
||||
OpenFeature.setProviderAndWait('1.7.7', neverReadyProvider).catch(() => {
|
||||
|
@ -466,45 +464,45 @@ describe('OpenFeatureClient', () => {
|
|||
expect(details.errorCode).toEqual(ErrorCode.PROVIDER_NOT_READY);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Evaluation details structure', () => {
|
||||
const flagKey = 'number-details';
|
||||
const defaultValue = 1970;
|
||||
let details: EvaluationDetails<number>;
|
||||
|
||||
|
||||
describe('Normal execution', () => {
|
||||
beforeEach(() => {
|
||||
const client = OpenFeature.getClient();
|
||||
details = client.getNumberDetails(flagKey, defaultValue);
|
||||
|
||||
|
||||
expect(details).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.2, 1.4.3', () => {
|
||||
it('should contain flag value', () => {
|
||||
expect(details.value).toEqual(NUMBER_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.4', () => {
|
||||
it('should contain flag key', () => {
|
||||
expect(details.flagKey).toEqual(flagKey);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.5', () => {
|
||||
it('should contain flag variant', () => {
|
||||
expect(details.variant).toEqual(NUMBER_VARIANT);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.6', () => {
|
||||
it('should contain reason', () => {
|
||||
expect(details.reason).toEqual(REASON);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Abnormal execution', () => {
|
||||
const NON_OPEN_FEATURE_ERROR_MESSAGE = 'A null dereference or something, I dunno.';
|
||||
const OPEN_FEATURE_ERROR_MESSAGE = "This ain't the flag you're looking for.";
|
||||
|
@ -524,14 +522,14 @@ describe('OpenFeatureClient', () => {
|
|||
} as unknown as Provider;
|
||||
const defaultNumberValue = 123;
|
||||
const defaultStringValue = 'hey!';
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
OpenFeature.setProvider(errorProvider);
|
||||
client = OpenFeature.getClient();
|
||||
nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue);
|
||||
openFeatureErrorDetails = client.getStringDetails('some-flag', defaultStringValue);
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.7', () => {
|
||||
describe('OpenFeatureError', () => {
|
||||
it('should contain error code', () => {
|
||||
|
@ -539,7 +537,7 @@ describe('OpenFeatureClient', () => {
|
|||
expect(openFeatureErrorDetails.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); // should get code from thrown OpenFeatureError
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Non-OpenFeatureError', () => {
|
||||
it('should contain error code', () => {
|
||||
expect(nonOpenFeatureErrorDetails.errorCode).toBeTruthy();
|
||||
|
@ -547,30 +545,30 @@ describe('OpenFeatureClient', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.8', () => {
|
||||
it('should contain error reason', () => {
|
||||
expect(nonOpenFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
|
||||
expect(openFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.9', () => {
|
||||
it('must not throw, must return default', () => {
|
||||
nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue);
|
||||
|
||||
|
||||
expect(nonOpenFeatureErrorDetails).toBeTruthy();
|
||||
expect(nonOpenFeatureErrorDetails.value).toEqual(defaultNumberValue);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.12', () => {
|
||||
describe('OpenFeatureError', () => {
|
||||
it('should contain "error" message', () => {
|
||||
expect(openFeatureErrorDetails.errorMessage).toEqual(OPEN_FEATURE_ERROR_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Non-OpenFeatureError', () => {
|
||||
it('should contain "error" message', () => {
|
||||
expect(nonOpenFeatureErrorDetails.errorMessage).toEqual(NON_OPEN_FEATURE_ERROR_MESSAGE);
|
||||
|
@ -578,14 +576,14 @@ describe('OpenFeatureClient', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Requirement 1.4.13, Requirement 1.4.14', () => {
|
||||
it('should return immutable `flag metadata` as defined by the provider', () => {
|
||||
const flagMetadata = {
|
||||
url: 'https://test.dev',
|
||||
version: '1',
|
||||
};
|
||||
|
||||
|
||||
const flagMetadataProvider = {
|
||||
metadata: {
|
||||
name: 'flag-metadata',
|
||||
|
@ -597,14 +595,14 @@ describe('OpenFeatureClient', () => {
|
|||
};
|
||||
}),
|
||||
} as unknown as Provider;
|
||||
|
||||
|
||||
OpenFeature.setProvider(flagMetadataProvider);
|
||||
const client = OpenFeature.getClient();
|
||||
const response = client.getBooleanDetails('some-flag', false);
|
||||
expect(response.flagMetadata).toBe(flagMetadata);
|
||||
expect(Object.isFrozen(response.flagMetadata)).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should return empty `flag metadata` because it was not set by the provider', () => {
|
||||
// The mock provider doesn't contain flag metadata
|
||||
OpenFeature.setProvider(MOCK_PROVIDER);
|
||||
|
@ -635,64 +633,5 @@ describe('OpenFeatureClient', () => {
|
|||
expect(OpenFeature.getClient().providerStatus).toEqual(ProviderStatus.READY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
describe('Requirement 2.7.1, Requirement 6.1.2.1', () => {
|
||||
const eventName = 'test-tracking-event';
|
||||
const trackingValue = 1234;
|
||||
const trackingDetails: TrackingEventDetails = {
|
||||
value: trackingValue,
|
||||
};
|
||||
const contextKey = 'key';
|
||||
const contextValue = 'val';
|
||||
|
||||
it('should no-op and not throw if tracking not defined on provider', async () => {
|
||||
await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER, track: undefined });
|
||||
const client = OpenFeature.getClient();
|
||||
|
||||
expect(() => {
|
||||
client.track(eventName, trackingDetails);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('provide empty tracking details to provider if not supplied in call', async () => {
|
||||
await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER });
|
||||
const client = OpenFeature.getClient();
|
||||
client.track(eventName);
|
||||
|
||||
expect(MOCK_PROVIDER.track).toHaveBeenCalledWith(
|
||||
eventName,
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should no-op and not throw if provider throws', async () => {
|
||||
await OpenFeature.setProviderAndWait({
|
||||
...MOCK_PROVIDER,
|
||||
track: () => {
|
||||
throw new Error('fake error');
|
||||
},
|
||||
});
|
||||
const client = OpenFeature.getClient();
|
||||
|
||||
expect(() => {
|
||||
client.track(eventName, trackingDetails);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should call provider with correct context', async () => {
|
||||
await OpenFeature.setProviderAndWait({ ...MOCK_PROVIDER });
|
||||
await OpenFeature.setContext({ [contextKey]: contextValue });
|
||||
const client = OpenFeature.getClient();
|
||||
client.track(eventName, trackingDetails);
|
||||
|
||||
expect(MOCK_PROVIDER.track).toHaveBeenCalledWith(
|
||||
eventName,
|
||||
expect.objectContaining({ [contextKey]: contextValue }),
|
||||
expect.objectContaining({ value: trackingValue }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
import type { EvaluationContext, JsonValue, Provider, ProviderMetadata, ResolutionDetails } from '../src';
|
||||
import { OpenFeature } from '../src';
|
||||
|
||||
const initializeMock = jest.fn();
|
||||
import { EvaluationContext, JsonValue, OpenFeature, Provider, ProviderMetadata, ResolutionDetails } from '../src';
|
||||
|
||||
class MockProvider implements Provider {
|
||||
readonly metadata: ProviderMetadata;
|
||||
|
@ -10,8 +7,6 @@ class MockProvider implements Provider {
|
|||
this.metadata = { name: options?.name ?? 'mock-provider' };
|
||||
}
|
||||
|
||||
initialize = initializeMock;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
|
||||
return Promise.resolve();
|
||||
|
@ -20,7 +15,7 @@ class MockProvider implements Provider {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
resolveBooleanEvaluation = jest.fn((flagKey: string, defaultValue: boolean, context: EvaluationContext) => {
|
||||
return {
|
||||
value: true,
|
||||
value: true
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -40,7 +35,6 @@ class MockProvider implements Provider {
|
|||
describe('Evaluation Context', () => {
|
||||
afterEach(async () => {
|
||||
await OpenFeature.clearContexts();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Requirement 3.2.2', () => {
|
||||
|
@ -65,42 +59,6 @@ describe('Evaluation Context', () => {
|
|||
expect(OpenFeature.getContext('invalid')).toEqual(defaultContext);
|
||||
});
|
||||
|
||||
describe('Set context during provider registration', () => {
|
||||
it('should set the context for the default provider', () => {
|
||||
const context: EvaluationContext = { property1: false };
|
||||
const provider = new MockProvider();
|
||||
OpenFeature.setProvider(provider, context);
|
||||
expect(OpenFeature.getContext()).toEqual(context);
|
||||
});
|
||||
|
||||
it('should set the context for a domain', async () => {
|
||||
const context: EvaluationContext = { property1: false };
|
||||
const domain = 'test';
|
||||
const provider = new MockProvider({ name: domain });
|
||||
OpenFeature.setProvider(domain, provider, context);
|
||||
expect(OpenFeature.getContext()).toEqual({});
|
||||
expect(OpenFeature.getContext(domain)).toEqual(context);
|
||||
});
|
||||
|
||||
it('should set the context for the default provider prior to initialization', async () => {
|
||||
const context: EvaluationContext = { property1: false };
|
||||
const provider = new MockProvider();
|
||||
await OpenFeature.setProviderAndWait(provider, context);
|
||||
expect(initializeMock).toHaveBeenCalledWith(context);
|
||||
expect(OpenFeature.getContext()).toEqual(context);
|
||||
});
|
||||
|
||||
it('should set the context for a domain prior to initialization', async () => {
|
||||
const context: EvaluationContext = { property1: false };
|
||||
const domain = 'test';
|
||||
const provider = new MockProvider({ name: domain });
|
||||
await OpenFeature.setProviderAndWait(domain, provider, context);
|
||||
expect(OpenFeature.getContext()).toEqual({});
|
||||
expect(OpenFeature.getContext(domain)).toEqual(context);
|
||||
expect(initializeMock).toHaveBeenCalledWith(context);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context Management', () => {
|
||||
it('should reset global context', async () => {
|
||||
const globalContext: EvaluationContext = { scope: 'global' };
|
|
@ -1,18 +1,16 @@
|
|||
import type { EventDetails } from '@openfeature/core';
|
||||
import { EventDetails } from '@openfeature/core';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type {
|
||||
JsonValue,
|
||||
Provider,
|
||||
ProviderMetadata,
|
||||
ResolutionDetails,
|
||||
StaleEvent
|
||||
} from '../src';
|
||||
import {
|
||||
JsonValue,
|
||||
NOOP_PROVIDER,
|
||||
OpenFeature,
|
||||
OpenFeatureEventEmitter,
|
||||
Provider,
|
||||
ProviderEvents,
|
||||
ProviderStatus
|
||||
ProviderMetadata,
|
||||
ProviderStatus,
|
||||
ResolutionDetails,
|
||||
StaleEvent
|
||||
} from '../src';
|
||||
|
||||
const TIMEOUT = 1000;
|
||||
|
@ -24,12 +22,11 @@ class MockProvider implements Provider {
|
|||
readonly runsOn = 'client';
|
||||
private hasInitialize: boolean;
|
||||
private hasContextChanged: boolean;
|
||||
private asyncContextChangedHandler: boolean;
|
||||
private failOnInit: boolean;
|
||||
private failOnContextChange: boolean;
|
||||
private asyncDelay?: number;
|
||||
private enableEvents: boolean;
|
||||
onContextChange?: () => Promise<void> | void;
|
||||
onContextChange?: () => Promise<void>;
|
||||
initialize?: () => Promise<void>;
|
||||
|
||||
constructor(options?: {
|
||||
|
@ -38,7 +35,6 @@ class MockProvider implements Provider {
|
|||
enableEvents?: boolean;
|
||||
failOnInit?: boolean;
|
||||
hasContextChanged?: boolean;
|
||||
asyncContextChangedHandler?: boolean;
|
||||
failOnContextChange?: boolean;
|
||||
name?: string;
|
||||
}) {
|
||||
|
@ -49,7 +45,6 @@ class MockProvider implements Provider {
|
|||
this.enableEvents = options?.enableEvents ?? true;
|
||||
this.failOnInit = options?.failOnInit ?? false;
|
||||
this.failOnContextChange = options?.failOnContextChange ?? false;
|
||||
this.asyncContextChangedHandler = options?.asyncContextChangedHandler ?? true;
|
||||
if (this.hasContextChanged) {
|
||||
this.onContextChange = this.changeHandler;
|
||||
}
|
||||
|
@ -85,19 +80,15 @@ class MockProvider implements Provider {
|
|||
}
|
||||
|
||||
private changeHandler() {
|
||||
if (this.asyncContextChangedHandler) {
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this.failOnContextChange) {
|
||||
reject(new Error(ERR_MESSAGE));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}, this.asyncDelay),
|
||||
);
|
||||
} else if (this.failOnContextChange) {
|
||||
throw new Error(ERR_MESSAGE);
|
||||
}
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
if (this.failOnContextChange) {
|
||||
reject(new Error(ERR_MESSAGE));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}, this.asyncDelay),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,21 +467,7 @@ describe('Events', () => {
|
|||
expect(OpenFeature.getHandlers(eventType)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('The event handler can be removed using an abort signal', () => {
|
||||
const abortController = new AbortController();
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const eventType = ProviderEvents.Stale;
|
||||
|
||||
OpenFeature.addHandler(eventType, handler1, { signal: abortController.signal });
|
||||
OpenFeature.addHandler(eventType, handler2);
|
||||
expect(OpenFeature.getHandlers(eventType)).toHaveLength(2);
|
||||
|
||||
abortController.abort();
|
||||
expect(OpenFeature.getHandlers(eventType)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('The API provides a function allowing the removal of event handlers from client', () => {
|
||||
it('The API provides a function allowing the removal of event handlers', () => {
|
||||
const client = OpenFeature.getClient(domain);
|
||||
const handler = jest.fn();
|
||||
const eventType = ProviderEvents.Stale;
|
||||
|
@ -500,21 +477,6 @@ describe('Events', () => {
|
|||
client.removeHandler(eventType, handler);
|
||||
expect(client.getHandlers(eventType)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('The event handler on the client can be removed using an abort signal', () => {
|
||||
const abortController = new AbortController();
|
||||
const client = OpenFeature.getClient(domain);
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const eventType = ProviderEvents.Stale;
|
||||
|
||||
client.addHandler(eventType, handler1, { signal: abortController.signal });
|
||||
client.addHandler(eventType, handler2);
|
||||
expect(client.getHandlers(eventType)).toHaveLength(2);
|
||||
|
||||
abortController.abort();
|
||||
expect(client.getHandlers(eventType)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Requirement 5.3.1', () => {
|
||||
|
@ -636,25 +598,6 @@ describe('Events', () => {
|
|||
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Reconciling events are not emitted for synchronous onContextChange operations', async () => {
|
||||
const provider = new MockProvider({
|
||||
hasInitialize: false,
|
||||
hasContextChanged: true,
|
||||
asyncContextChangedHandler: false,
|
||||
});
|
||||
|
||||
const reconcileHandler = jest.fn(() => {});
|
||||
const changedEventHandler = jest.fn(() => {});
|
||||
|
||||
await OpenFeature.setProviderAndWait(domain, provider);
|
||||
OpenFeature.addHandler(ProviderEvents.Reconciling, reconcileHandler);
|
||||
OpenFeature.addHandler(ProviderEvents.ContextChanged, changedEventHandler);
|
||||
await OpenFeature.setContext(domain, {});
|
||||
|
||||
expect(reconcileHandler).not.toHaveBeenCalled();
|
||||
expect(changedEventHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider has no context changed handler', () => {
|
||||
|
@ -672,7 +615,7 @@ describe('Events', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('client', () => {
|
||||
describe('provider has context changed handler', () => {
|
||||
it('Stale and ContextChanged are emitted', async () => {
|
||||
|
@ -867,9 +810,12 @@ describe('Events', () => {
|
|||
};
|
||||
|
||||
client.addHandler(ProviderEvents.ContextChanged, handler);
|
||||
|
||||
|
||||
// update context change twice
|
||||
await Promise.all([OpenFeature.setContext(domain, {}), OpenFeature.setContext(domain, {})]);
|
||||
await Promise.all([
|
||||
OpenFeature.setContext(domain, {}),
|
||||
OpenFeature.setContext(domain, {}),
|
||||
]);
|
||||
|
||||
// should only have run once
|
||||
expect(runs).toEqual(1);
|
|
@ -1,5 +1,13 @@
|
|||
import type { Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
|
||||
import { GeneralError, OpenFeature, StandardResolutionReasons, ErrorCode } from '../src';
|
||||
import {
|
||||
Provider,
|
||||
ResolutionDetails,
|
||||
Client,
|
||||
FlagValueType,
|
||||
EvaluationContext,
|
||||
GeneralError,
|
||||
OpenFeature,
|
||||
Hook,
|
||||
} from '../src';
|
||||
|
||||
const BOOLEAN_VALUE = true;
|
||||
|
||||
|
@ -69,39 +77,6 @@ describe('Hooks', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
it('client metadata and provider metadata must match the client and provider used to resolve the flag', (done) => {
|
||||
const provider: Provider = {
|
||||
metadata: {
|
||||
name: 'mock-my-domain-provider',
|
||||
},
|
||||
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||
return Promise.resolve({
|
||||
value: BOOLEAN_VALUE,
|
||||
variant: BOOLEAN_VARIANT,
|
||||
reason: REASON,
|
||||
});
|
||||
}),
|
||||
} as unknown as Provider;
|
||||
|
||||
OpenFeature.setProvider('my-domain', provider);
|
||||
const client = OpenFeature.getClient('my-domain');
|
||||
|
||||
client.getBooleanValue(FLAG_KEY, false, {
|
||||
hooks: [
|
||||
{
|
||||
before: (hookContext) => {
|
||||
try {
|
||||
expect(hookContext.providerMetadata).toEqual(provider.metadata);
|
||||
expect(hookContext.clientMetadata).toEqual(client.metadata);
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Requirement 4.1.3', () => {
|
||||
|
@ -198,27 +173,6 @@ describe('Hooks', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('"error" must run if resolution details contains an error code', () => {
|
||||
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockReturnValue({
|
||||
value: BOOLEAN_VALUE,
|
||||
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||
});
|
||||
|
||||
const mockErrorHook = jest.fn();
|
||||
|
||||
const details = client.getBooleanDetails(FLAG_KEY, false, {
|
||||
hooks: [{ error: mockErrorHook }],
|
||||
});
|
||||
|
||||
expect(mockErrorHook).toHaveBeenCalled();
|
||||
expect(details).toEqual(
|
||||
expect.objectContaining({
|
||||
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -271,25 +225,6 @@ describe('Hooks', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Requirement 4.3.8', () => {
|
||||
it('"evaluation details" passed to the "finally" stage matches the evaluation details returned to the application author', () => {
|
||||
OpenFeature.setProvider(MOCK_PROVIDER);
|
||||
let evaluationDetailsHooks;
|
||||
|
||||
const evaluationDetails = client.getBooleanDetails(FLAG_KEY, false, {
|
||||
hooks: [
|
||||
{
|
||||
finally: (_, details) => {
|
||||
evaluationDetailsHooks = details;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(evaluationDetailsHooks).toEqual(evaluationDetails);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Requirement 4.4.2', () => {
|
||||
|
@ -767,14 +702,14 @@ describe('Hooks', () => {
|
|||
done(err);
|
||||
}
|
||||
},
|
||||
after: (_hookContext, _evaluationDetails, hookHints) => {
|
||||
after: (_hookContext, _evaluationDetils, hookHints) => {
|
||||
try {
|
||||
expect(hookHints?.hint).toBeTruthy();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
},
|
||||
finally: (_, _evaluationDetails, hookHints) => {
|
||||
finally: (_, hookHints) => {
|
||||
try {
|
||||
expect(hookHints?.hint).toBeTruthy();
|
||||
done();
|
|
@ -1,7 +1,44 @@
|
|||
import { FlagNotFoundError, InMemoryProvider, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '../src';
|
||||
import { FlagNotFoundError, GeneralError, InMemoryProvider, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '../src';
|
||||
import { FlagConfiguration } from '../src/provider/in-memory-provider/flag-configuration';
|
||||
import { VariantNotFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
|
||||
|
||||
describe('in-memory provider', () => {
|
||||
describe('initialize', () => {
|
||||
it('Should not throw for valid flags', async () => {
|
||||
const booleanFlagSpec = {
|
||||
'a-boolean-flag': {
|
||||
variants: {
|
||||
on: true,
|
||||
off: false,
|
||||
},
|
||||
disabled: false,
|
||||
defaultVariant: 'on',
|
||||
},
|
||||
};
|
||||
const provider = new InMemoryProvider(booleanFlagSpec);
|
||||
await provider.initialize();
|
||||
});
|
||||
|
||||
it('Should throw on invalid flags', async () => {
|
||||
const throwingFlagSpec: FlagConfiguration = {
|
||||
'a-boolean-flag': {
|
||||
variants: {
|
||||
on: true,
|
||||
off: false,
|
||||
},
|
||||
disabled: false,
|
||||
defaultVariant: 'on',
|
||||
contextEvaluator: () => {
|
||||
throw new GeneralError('context eval error');
|
||||
},
|
||||
},
|
||||
};
|
||||
const provider = new InMemoryProvider(throwingFlagSpec);
|
||||
const someContext = {};
|
||||
await expect(provider.initialize(someContext)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean flags', () => {
|
||||
const provider = new InMemoryProvider({});
|
||||
it('resolves to default variant with reason static', async () => {
|
||||
|
@ -494,6 +531,8 @@ describe('in-memory provider', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
|
||||
const firstResolution = provider.resolveStringEvaluation('some-flag', 'deafaultFirstResolution');
|
||||
|
||||
expect(firstResolution).toEqual({
|
||||
|
@ -538,6 +577,8 @@ describe('in-memory provider', () => {
|
|||
|
||||
const provider = new InMemoryProvider(flagsSpec);
|
||||
|
||||
await provider.initialize();
|
||||
|
||||
// I passed configuration by reference, so maybe I can mess
|
||||
// with it behind the providers back!
|
||||
flagsSpec['some-flag'] = substituteSpec;
|
|
@ -1,7 +1,5 @@
|
|||
import type { Paradigm } from '@openfeature/core';
|
||||
import type { Provider} from '../src';
|
||||
import { OpenFeature, OpenFeatureAPI, ProviderStatus } from '../src';
|
||||
import { OpenFeatureClient } from '../src/client/internal/open-feature-client';
|
||||
import { Paradigm } from '@openfeature/core';
|
||||
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src';
|
||||
|
||||
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
|
||||
return {
|
||||
|
@ -75,8 +73,8 @@ describe('OpenFeature', () => {
|
|||
it('should set the default provider if no domain is provided', () => {
|
||||
const provider = mockProvider();
|
||||
OpenFeature.setProvider(provider);
|
||||
const registeredProvider = OpenFeature.getProvider();
|
||||
expect(registeredProvider).toEqual(provider);
|
||||
const client = OpenFeature.getClient();
|
||||
expect(client.metadata.providerMetadata.name).toEqual(provider.metadata.name);
|
||||
});
|
||||
|
||||
it('should not change providers associated with a domain when setting a new default provider', () => {
|
||||
|
@ -86,11 +84,11 @@ describe('OpenFeature', () => {
|
|||
OpenFeature.setProvider(provider);
|
||||
OpenFeature.setProvider(domain, fakeProvider);
|
||||
|
||||
const defaultProvider = OpenFeature.getProvider();
|
||||
const domainSpecificProvider = OpenFeature.getProvider(domain);
|
||||
const defaultClient = OpenFeature.getClient();
|
||||
const domainSpecificClient = OpenFeature.getClient(domain);
|
||||
|
||||
expect(defaultProvider).toEqual(provider);
|
||||
expect(domainSpecificProvider).toEqual(fakeProvider);
|
||||
expect(defaultClient.metadata.providerMetadata.name).toEqual(provider.metadata.name);
|
||||
expect(domainSpecificClient.metadata.providerMetadata.name).toEqual(fakeProvider.metadata.name);
|
||||
});
|
||||
|
||||
it('should bind a new provider to existing clients in a matching domain', () => {
|
|
@ -9,9 +9,9 @@
|
|||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
/* Language and Environment */
|
||||
"target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"ES2022",
|
||||
"DOM"
|
||||
], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
|
@ -24,7 +24,7 @@
|
|||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
/* Modules */
|
||||
"module": "ES2015", /* Specify what module code is generated. */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
@ -1,110 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## [0.2.5](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.4...nestjs-sdk-v0.2.5) (2025-05-27)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* adds RequireFlagsEnabled decorator ([#1159](https://github.com/open-feature/js-sdk/issues/1159)) ([59b8fe9](https://github.com/open-feature/js-sdk/commit/59b8fe904f053e4aa3d0c72631af34183ff54dc7))
|
||||
|
||||
|
||||
## [0.2.4](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.3...nestjs-sdk-v0.2.4) (2025-04-20)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **nest:** allow nestjs version 11 ([#1176](https://github.com/open-feature/js-sdk/issues/1176)) ([42a3b39](https://github.com/open-feature/js-sdk/commit/42a3b39c2488002f249b37ce86794ef2f77eb31c))
|
||||
|
||||
## [0.2.3](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.2...nestjs-sdk-v0.2.3) (2025-04-11)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* update sdk peer ([#1142](https://github.com/open-feature/js-sdk/issues/1142)) ([8bb6206](https://github.com/open-feature/js-sdk/commit/8bb620601e2b8dc7b62d717169b585bd1c886996))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* devDependencies
|
||||
* @openfeature/server-sdk bumped from * to 1.18.0
|
||||
|
||||
## [0.2.2](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.1-experimental...nestjs-sdk-v0.2.2) (2024-10-29)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* import type lint rule and fixes ([#1039](https://github.com/open-feature/js-sdk/issues/1039)) ([01fcb93](https://github.com/open-feature/js-sdk/commit/01fcb933d2cbd131a0f4a005173cdd1906087e18))
|
||||
|
||||
## [0.2.1-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.2.0-experimental...nestjs-sdk-v0.2.1-experimental) (2024-06-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* lower compilation target to es2015 ([#957](https://github.com/open-feature/js-sdk/issues/957)) ([c2d6c17](https://github.com/open-feature/js-sdk/commit/c2d6c1761ae19f937deaff2f011a0380f8af7350))
|
||||
|
||||
## [0.2.0-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.5-experimental...nestjs-sdk-v0.2.0-experimental) (2024-05-19)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* rename FeatureClient decorator to OpenFeatureClient ([#949](https://github.com/open-feature/js-sdk/issues/949))
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* rename FeatureClient decorator to OpenFeatureClient ([#949](https://github.com/open-feature/js-sdk/issues/949)) ([a531238](https://github.com/open-feature/js-sdk/commit/a531238124510e6aa150ff619972f8880346507b))
|
||||
|
||||
## [0.1.5-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.4-experimental...nestjs-sdk-v0.1.5-experimental) (2024-05-13)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* removes exports of OpenFeatureClient class and makes event props readonly ([#918](https://github.com/open-feature/js-sdk/issues/918)) ([e9a25c2](https://github.com/open-feature/js-sdk/commit/e9a25c21cb17c3b5700bca652e3c0ed15e8f49b4))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* remove node 16 ([#875](https://github.com/open-feature/js-sdk/issues/875)) ([c1878e4](https://github.com/open-feature/js-sdk/commit/c1878e4effac3c8c9aa8a34cee4214f628a1e4ca))
|
||||
* **deps:** update dependency supertest to v7 ([#939](https://github.com/open-feature/js-sdk/issues/939)) ([9083df8](https://github.com/open-feature/js-sdk/commit/9083df8463d6f970111dedee114aedc0a20e2a3c))
|
||||
|
||||
## [0.1.4-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.3-experimental...nestjremove node 16 ([#875](https://github.com/open-feature/js-sdk/issues/875)) ([c1878e4](https://github.com/open-feature/js-sdk/commit/c1878e4effac3c8c9aa8a34cee4214f628a1e4ca))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* removes exports of OpenFeatureClient class and makes event props readonly ([#918](https://github.com/open-feature/js-sdk/issues/918)) ([e9a25c2](https://github.com/open-feature/js-sdk/commit/e9a25c21cb17c3b5700bca652e3c0ed15e8f49b4))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* **deps:** update dependency supertest to v7 ([#939](https://github.com/open-feature/js-sdk/issues/939)) ([9083df8](https://github.com/open-feature/js-sdk/commit/9083df8463d6f970111dedee114aedc0a20e2a3c))
|
||||
|
||||
## [0.1.4-experimental](https://github.com/open-feature/js-sdk/compare/s-sdk-v0.1.4-experimental) (2024-04-18)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* bump spec version badge to v0.8.0 ([#910](https://github.com/open-feature/js-sdk/issues/910)) ([a7b2c4b](https://github.com/open-feature/js-sdk/commit/a7b2c4bca09112d49e637735466502adb1438ebe))
|
||||
|
||||
## [0.1.3-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.2-experimental...nestjs-sdk-v0.1.3-experimental) (2024-04-02)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **deps:** resolve CVE-2024-29041 with nest update ([#889](https://github.com/open-feature/js-sdk/issues/889)) ([042ec5f](https://github.com/open-feature/js-sdk/commit/042ec5f70863ecc974371481be24f08c65321f7c))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* remove duplicate npm install section ([fd0fcfc](https://github.com/open-feature/js-sdk/commit/fd0fcfcfc815803a967e971b7e575c24e46c93bc))
|
||||
|
||||
## [0.1.2-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.1-experimental...nestjs-sdk-v0.1.2-experimental) (2024-03-06)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **types:** conflicts with peer types ([#852](https://github.com/open-feature/js-sdk/issues/852)) ([fdc8576](https://github.com/open-feature/js-sdk/commit/fdc8576f472253604e26c36e10c0d315f71dbe1c))
|
||||
|
||||
## [0.1.1-experimental](https://github.com/open-feature/js-sdk/compare/nestjs-sdk-v0.1.0-experimental...nestjs-sdk-v0.1.1-experimental) (2024-03-05)
|
||||
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-start-version -->
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/nestjs-sdk-v0.2.5">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.2.5&color=blue&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/nestjs-sdk-v0.1.1-experimental">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.1.1-experimental&color=blue&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-end -->
|
||||
<br/>
|
||||
|
@ -50,7 +50,7 @@ Capabilities include:
|
|||
|
||||
### Requirements
|
||||
|
||||
- Node.js version 20+
|
||||
- Node.js version 16+
|
||||
- NestJS version 8+
|
||||
|
||||
### Install
|
||||
|
@ -61,21 +61,14 @@ Capabilities include:
|
|||
npm install --save @openfeature/nestjs-sdk
|
||||
```
|
||||
|
||||
#### yarn
|
||||
|
||||
```sh
|
||||
# yarn requires manual installation of the peer dependencies (see below)
|
||||
yarn add @openfeature/nestjs-sdk @openfeature/server-sdk @openfeature/core
|
||||
```
|
||||
|
||||
#### Required peer dependencies
|
||||
|
||||
The following list contains the peer dependencies of `@openfeature/nestjs-sdk` with its expected and compatible versions:
|
||||
|
||||
- `@openfeature/server-sdk`: >=1.7.5
|
||||
- `@nestjs/common`: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||
- `@nestjs/core`: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||
- `rxjs`: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
* `@openfeature/server-sdk`: >=1.7.5
|
||||
* `@nestjs/common`: ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||
* `@nestjs/core`: ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||
* `rxjs`: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
The minimum required version of `@openfeature/server-sdk` currently is `1.7.5`.
|
||||
|
||||
|
@ -137,13 +130,13 @@ It is also possible to inject the default or domain scoped OpenFeature clients i
|
|||
|
||||
```ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OpenFeatureClient, Client } from '@openfeature/nestjs-sdk';
|
||||
import { FeatureClient, Client } from '@openfeature/nestjs-sdk';
|
||||
|
||||
@Injectable()
|
||||
export class OpenFeatureTestService {
|
||||
constructor(
|
||||
@OpenFeatureClient() private defaultClient: Client,
|
||||
@OpenFeatureClient({ domain: 'my-domain' }) private scopedClient: Client,
|
||||
@FeatureClient() private defaultClient: Client,
|
||||
@FeatureClient({ domain: 'my-domain' }) private scopedClient: Client,
|
||||
) {}
|
||||
|
||||
public async getBoolean() {
|
||||
|
@ -152,24 +145,6 @@ export class OpenFeatureTestService {
|
|||
}
|
||||
```
|
||||
|
||||
#### Managing Controller or Route Access via Feature Flags
|
||||
|
||||
The `RequireFlagsEnabled` decorator can be used to manage access to a controller or route based on the enabled state of a feature flag. The decorator will throw an exception if the required feature flag(s) are not enabled.
|
||||
|
||||
```ts
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { RequireFlagsEnabled } from '@openfeature/nestjs-sdk';
|
||||
|
||||
@Controller()
|
||||
export class OpenFeatureController {
|
||||
@RequireFlagsEnabled({ flags: [{ flagKey: 'testBooleanFlag' }] })
|
||||
@Get('/welcome')
|
||||
public async welcome() {
|
||||
return 'Welcome to this OpenFeature-enabled NestJS app!';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module additional information
|
||||
|
||||
### Flag evaluation context injection
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openfeature/nestjs-sdk",
|
||||
"version": "0.2.5",
|
||||
"version": "0.1.1-experimental",
|
||||
"description": "OpenFeature Nest.js SDK",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"files": [
|
||||
|
@ -16,10 +16,9 @@
|
|||
"scripts": {
|
||||
"test": "jest --verbose",
|
||||
"lint": "eslint ./",
|
||||
"lint:fix": "eslint ./ --fix",
|
||||
"clean": "shx rm -rf ./dist",
|
||||
"build:esm": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2015 --platform=node --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:cjs": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2015 --platform=node --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:esm": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2022 --platform=node --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:cjs": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2022 --platform=node --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:rollup-types": "rollup -c ../../rollup.config.mjs",
|
||||
"build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:rollup-types",
|
||||
"postbuild": "shx cp ./../../package.esm.json ./dist/esm/package.json",
|
||||
|
@ -46,19 +45,19 @@
|
|||
},
|
||||
"homepage": "https://github.com/open-feature/js-sdk#readme",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"rxjs": "^6.0.0 || ^7.0.0 || 8.0.0",
|
||||
"@openfeature/server-sdk": "^1.17.1"
|
||||
"@openfeature/server-sdk": ">=1.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "^11.0.20",
|
||||
"@nestjs/core": "^11.0.20",
|
||||
"@nestjs/platform-express": "^11.0.20",
|
||||
"@nestjs/testing": "^11.0.20",
|
||||
"@nestjs/common": "^10.2.10",
|
||||
"@nestjs/core": "^10.2.10",
|
||||
"@nestjs/platform-express": "^10.2.10",
|
||||
"@nestjs/testing": "^10.2.10",
|
||||
"@openfeature/core": "*",
|
||||
"@openfeature/server-sdk": "1.18.0",
|
||||
"@openfeature/server-sdk": "*",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"supertest": "^7.0.0"
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { EvaluationContext } from '@openfeature/core';
|
||||
import type { ExecutionContext} from '@nestjs/common';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { EvaluationContext } from '@openfeature/core';
|
||||
import { ExecutionContext, Inject } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* A factory function for creating an OpenFeature {@link EvaluationContext} from Nest {@link ExecutionContext}.
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ContextFactory} from './context-factory';
|
||||
import { ContextFactoryToken } from './context-factory';
|
||||
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { ContextFactory, ContextFactoryToken } from './context-factory';
|
||||
import { Observable } from 'rxjs';
|
||||
import { OpenFeature } from '@openfeature/server-sdk';
|
||||
import { OpenFeatureModule } from './open-feature.module';
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { createParamDecorator, Inject } from '@nestjs/common';
|
||||
import type { EvaluationContext, EvaluationDetails, FlagValue, JsonValue } from '@openfeature/server-sdk';
|
||||
import { Client } from '@openfeature/server-sdk';
|
||||
import {
|
||||
EvaluationContext,
|
||||
EvaluationDetails,
|
||||
FlagValue,
|
||||
JsonValue,
|
||||
OpenFeature,
|
||||
Client,
|
||||
} from '@openfeature/server-sdk';
|
||||
import { getOpenFeatureClientToken } from './open-feature.module';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from } from 'rxjs';
|
||||
import { getClientForEvaluation } from './utils';
|
||||
import { from, Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Options for injecting an OpenFeature client into a constructor.
|
||||
|
@ -22,7 +26,7 @@ interface FeatureClientProps {
|
|||
* @param {FeatureClientProps} [props] The options for injecting the client.
|
||||
* @returns {PropertyDecorator & ParameterDecorator} The decorator function.
|
||||
*/
|
||||
export const OpenFeatureClient = (props?: FeatureClientProps) => Inject(getOpenFeatureClientToken(props?.domain));
|
||||
export const FeatureClient = (props?: FeatureClientProps) => Inject(getOpenFeatureClientToken(props?.domain));
|
||||
|
||||
/**
|
||||
* Options for injecting a feature flag into a route handler.
|
||||
|
@ -50,6 +54,16 @@ interface FeatureProps<T extends FlagValue> {
|
|||
context?: EvaluationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a domain scoped or the default OpenFeature client with the given context.
|
||||
* @param {string} domain The domain of the OpenFeature client.
|
||||
* @param {EvaluationContext} context The evaluation context of the client.
|
||||
* @returns {Client} The OpenFeature client.
|
||||
*/
|
||||
function getClientForEvaluation(domain?: string, context?: EvaluationContext) {
|
||||
return domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route handler parameter decorator.
|
||||
*
|
||||
|
|
|
@ -2,6 +2,5 @@ export * from './open-feature.module';
|
|||
export * from './feature.decorator';
|
||||
export * from './evaluation-context-interceptor';
|
||||
export * from './context-factory';
|
||||
export * from './require-flags-enabled.decorator';
|
||||
// re-export the server-sdk so consumers can access that API from the nestjs-sdk
|
||||
export * from '@openfeature/server-sdk';
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
import type {
|
||||
import {
|
||||
DynamicModule,
|
||||
Module,
|
||||
FactoryProvider as NestFactoryProvider,
|
||||
ValueProvider,
|
||||
ClassProvider,
|
||||
Provider as NestProvider} from '@nestjs/common';
|
||||
import {
|
||||
Module,
|
||||
Provider as NestProvider,
|
||||
ExecutionContext,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
import {
|
||||
Client,
|
||||
Hook,
|
||||
OpenFeature,
|
||||
Provider,
|
||||
EvaluationContext,
|
||||
ServerProviderEvents,
|
||||
EventHandler,
|
||||
Logger} from '@openfeature/server-sdk';
|
||||
import {
|
||||
OpenFeature,
|
||||
Logger,
|
||||
AsyncLocalStorageTransactionContextPropagator,
|
||||
} from '@openfeature/server-sdk';
|
||||
import type { ContextFactory} from './context-factory';
|
||||
import { ContextFactoryToken } from './context-factory';
|
||||
import { ContextFactory, ContextFactoryToken } from './context-factory';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { EvaluationContextInterceptor } from './evaluation-context-interceptor';
|
||||
import { ShutdownService } from './shutdown.service';
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
import type { CallHandler, ExecutionContext, HttpException, NestInterceptor } from '@nestjs/common';
|
||||
import { applyDecorators, mixin, NotFoundException, UseInterceptors } from '@nestjs/common';
|
||||
import { getClientForEvaluation } from './utils';
|
||||
import type { EvaluationContext } from '@openfeature/server-sdk';
|
||||
import type { ContextFactory } from './context-factory';
|
||||
|
||||
type RequiredFlag = {
|
||||
flagKey: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for using one or more Boolean feature flags to control access to a Controller or Route.
|
||||
*/
|
||||
interface RequireFlagsEnabledProps {
|
||||
/**
|
||||
* The key and default value of the feature flag.
|
||||
* @see {@link Client#getBooleanValue}
|
||||
*/
|
||||
flags: RequiredFlag[];
|
||||
|
||||
/**
|
||||
* The exception to throw if any of the required feature flags are not enabled.
|
||||
* Defaults to a 404 Not Found exception.
|
||||
* @see {@link HttpException}
|
||||
* @default new NotFoundException(`Cannot ${req.method} ${req.url}`)
|
||||
*/
|
||||
exception?: HttpException;
|
||||
|
||||
/**
|
||||
* The domain of the OpenFeature client, if a domain scoped client should be used.
|
||||
* @see {@link OpenFeature#getClient}
|
||||
*/
|
||||
domain?: string;
|
||||
|
||||
/**
|
||||
* The {@link EvaluationContext} for evaluating the feature flag.
|
||||
* @see {@link OpenFeature#setContext}
|
||||
*/
|
||||
context?: EvaluationContext;
|
||||
|
||||
/**
|
||||
* A factory function for creating an OpenFeature {@link EvaluationContext} from Nest {@link ExecutionContext}.
|
||||
* For example, this can be used to get header info from an HTTP request or information from a gRPC call to be used in the {@link EvaluationContext}.
|
||||
* @see {@link ContextFactory}
|
||||
*/
|
||||
contextFactory?: ContextFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller or Route permissions handler decorator.
|
||||
*
|
||||
* Requires that the given feature flags are enabled for the request to be processed, else throws an exception.
|
||||
*
|
||||
* For example:
|
||||
* ```typescript
|
||||
* @RequireFlagsEnabled({
|
||||
* flags: [ // Required, an array of Boolean flags to check, with optional default values (defaults to false)
|
||||
* { flagKey: 'flagName' },
|
||||
* { flagKey: 'flagName2', defaultValue: true },
|
||||
* ],
|
||||
* exception: new ForbiddenException(), // Optional, defaults to a 404 Not Found Exception
|
||||
* domain: 'my-domain', // Optional, defaults to the default OpenFeature Client
|
||||
* context: { // Optional, defaults to the global OpenFeature Context
|
||||
* targetingKey: 'user-id',
|
||||
* },
|
||||
* contextFactory: (context: ExecutionContext) => { // Optional, defaults to the global OpenFeature Context. Takes precedence over the context option.
|
||||
* return {
|
||||
* targetingKey: context.switchToHttp().getRequest().headers['x-user-id'],
|
||||
* };
|
||||
* },
|
||||
* })
|
||||
* @Get('/')
|
||||
* public async handleGetRequest()
|
||||
* ```
|
||||
* @param {RequireFlagsEnabledProps} props The options for injecting the feature flag.
|
||||
* @returns {ClassDecorator & MethodDecorator} The decorator that can be used to require Boolean Feature Flags to be enabled for a controller or a specific route.
|
||||
*/
|
||||
export const RequireFlagsEnabled = (props: RequireFlagsEnabledProps): ClassDecorator & MethodDecorator =>
|
||||
applyDecorators(UseInterceptors(FlagsEnabledInterceptor(props)));
|
||||
|
||||
const FlagsEnabledInterceptor = (props: RequireFlagsEnabledProps) => {
|
||||
class FlagsEnabledInterceptor implements NestInterceptor {
|
||||
constructor() {}
|
||||
|
||||
async intercept(context: ExecutionContext, next: CallHandler) {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const evaluationContext = props.contextFactory ? await props.contextFactory(context) : props.context;
|
||||
const client = getClientForEvaluation(props.domain, evaluationContext);
|
||||
|
||||
for (const flag of props.flags) {
|
||||
const endpointAccessible = await client.getBooleanValue(flag.flagKey, flag.defaultValue ?? false);
|
||||
|
||||
if (!endpointAccessible) {
|
||||
throw props.exception || new NotFoundException(`Cannot ${req.method} ${req.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
|
||||
return mixin(FlagsEnabledInterceptor);
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { OpenFeature } from '@openfeature/server-sdk';
|
||||
|
||||
@Injectable()
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import type { Client, EvaluationContext } from '@openfeature/server-sdk';
|
||||
import { OpenFeature } from '@openfeature/server-sdk';
|
||||
|
||||
/**
|
||||
* Returns a domain scoped or the default OpenFeature client with the given context.
|
||||
* @param {string} domain The domain of the OpenFeature client.
|
||||
* @param {EvaluationContext} context The evaluation context of the client.
|
||||
* @returns {Client} The OpenFeature client.
|
||||
*/
|
||||
export function getClientForEvaluation(domain?: string, context?: EvaluationContext) {
|
||||
return domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { InMemoryProvider } from '@openfeature/server-sdk';
|
||||
import type { EvaluationContext } from '@openfeature/server-sdk';
|
||||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { OpenFeatureModule } from '../src';
|
||||
|
||||
export const defaultProvider = new InMemoryProvider({
|
||||
|
@ -24,17 +23,6 @@ export const defaultProvider = new InMemoryProvider({
|
|||
variants: { default: { client: 'default' } },
|
||||
disabled: false,
|
||||
},
|
||||
testBooleanFlag2: {
|
||||
defaultVariant: 'default',
|
||||
variants: { default: false, enabled: true },
|
||||
disabled: false,
|
||||
contextEvaluator: (ctx: EvaluationContext) => {
|
||||
if (ctx.targetingKey === '123') {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'default';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const providers = {
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import supertest from 'supertest';
|
||||
import {
|
||||
OpenFeatureController,
|
||||
OpenFeatureContextScopedController,
|
||||
OpenFeatureRequireFlagsEnabledController,
|
||||
OpenFeatureTestService,
|
||||
} from './test-app';
|
||||
import { OpenFeatureController, OpenFeatureControllerContextScopedController, OpenFeatureTestService } from './test-app';
|
||||
import { exampleContextFactory, getOpenFeatureDefaultTestModule } from './fixtures';
|
||||
import { OpenFeatureModule } from '../src';
|
||||
import { defaultProvider, providers } from './fixtures';
|
||||
|
@ -19,9 +13,11 @@ describe('OpenFeature SDK', () => {
|
|||
|
||||
beforeAll(async () => {
|
||||
moduleRef = await Test.createTestingModule({
|
||||
imports: [getOpenFeatureDefaultTestModule()],
|
||||
imports: [
|
||||
getOpenFeatureDefaultTestModule()
|
||||
],
|
||||
providers: [OpenFeatureTestService],
|
||||
controllers: [OpenFeatureController, OpenFeatureRequireFlagsEnabledController],
|
||||
controllers: [OpenFeatureController],
|
||||
}).compile();
|
||||
app = moduleRef.createNestApplication();
|
||||
app = await app.init();
|
||||
|
@ -115,7 +111,7 @@ describe('OpenFeature SDK', () => {
|
|||
});
|
||||
|
||||
describe('evaluation context service should', () => {
|
||||
it('inject the evaluation context from contex factory', async function () {
|
||||
it('inject the evaluation context from contex factory', async function() {
|
||||
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/dynamic-context-in-service')
|
||||
|
@ -125,77 +121,26 @@ describe('OpenFeature SDK', () => {
|
|||
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, { targetingKey: 'dynamic-user' }, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('require flags enabled decorator', () => {
|
||||
describe('OpenFeatureController', () => {
|
||||
it('should sucessfully return the response if the flag is enabled', async () => {
|
||||
await supertest(app.getHttpServer()).get('/flags-enabled').expect(200).expect('Get Boolean Flag Success!');
|
||||
});
|
||||
|
||||
it('should throw an exception if the flag is disabled', async () => {
|
||||
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
||||
value: false,
|
||||
reason: 'DISABLED',
|
||||
});
|
||||
await supertest(app.getHttpServer()).get('/flags-enabled').expect(404);
|
||||
});
|
||||
|
||||
it('should throw a custom exception if the flag is disabled', async () => {
|
||||
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
||||
value: false,
|
||||
reason: 'DISABLED',
|
||||
});
|
||||
await supertest(app.getHttpServer()).get('/flags-enabled-custom-exception').expect(403);
|
||||
});
|
||||
|
||||
it('should throw a custom exception if the flag is disabled with context', async () => {
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/flags-enabled-custom-exception-with-context')
|
||||
.set('x-user-id', '123')
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenFeatureControllerRequireFlagsEnabled', () => {
|
||||
it('should allow access to the RequireFlagsEnabled controller with global context interceptor', async () => {
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/require-flags-enabled')
|
||||
.set('x-user-id', '123')
|
||||
.expect(200)
|
||||
.expect('Hello, world!');
|
||||
});
|
||||
|
||||
it('should throw a 403 - Forbidden exception if user does not match targeting requirements', async () => {
|
||||
await supertest(app.getHttpServer()).get('/require-flags-enabled').set('x-user-id', 'not-123').expect(403);
|
||||
});
|
||||
|
||||
it('should throw a 403 - Forbidden exception if one of the flags is disabled', async () => {
|
||||
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
||||
value: false,
|
||||
reason: 'DISABLED',
|
||||
});
|
||||
await supertest(app.getHttpServer()).get('/require-flags-enabled').set('x-user-id', '123').expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without global context interceptor', () => {
|
||||
|
||||
let moduleRef: TestingModule;
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
moduleRef = await Test.createTestingModule({
|
||||
imports: [
|
||||
OpenFeatureModule.forRoot({
|
||||
contextFactory: exampleContextFactory,
|
||||
defaultProvider,
|
||||
providers,
|
||||
useGlobalInterceptor: false,
|
||||
useGlobalInterceptor: false
|
||||
}),
|
||||
],
|
||||
providers: [OpenFeatureTestService],
|
||||
controllers: [OpenFeatureController, OpenFeatureContextScopedController],
|
||||
controllers: [OpenFeatureController, OpenFeatureControllerContextScopedController],
|
||||
}).compile();
|
||||
app = moduleRef.createNestApplication();
|
||||
app = await app.init();
|
||||
|
@ -212,7 +157,7 @@ describe('OpenFeature SDK', () => {
|
|||
});
|
||||
|
||||
describe('evaluation context service should', () => {
|
||||
it('inject empty context if no context interceptor is configured', async function () {
|
||||
it('inject empty context if no context interceptor is configured', async function() {
|
||||
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/dynamic-context-in-service')
|
||||
|
@ -226,26 +171,9 @@ describe('OpenFeature SDK', () => {
|
|||
describe('With Controller bound Context interceptor', () => {
|
||||
it('should not use context if global context interceptor is not configured', async () => {
|
||||
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/controller-context')
|
||||
.set('x-user-id', '123')
|
||||
.expect(200)
|
||||
.expect('true');
|
||||
await supertest(app.getHttpServer()).get('/controller-context').set('x-user-id', '123').expect(200).expect('true');
|
||||
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, { targetingKey: '123' }, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('require flags enabled decorator', () => {
|
||||
it('should return a 404 - Not Found exception if the flag is disabled', async () => {
|
||||
jest.spyOn(providers.domainScopedClient, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
||||
value: false,
|
||||
reason: 'DISABLED',
|
||||
});
|
||||
await supertest(app.getHttpServer())
|
||||
.get('/controller-context/flags-enabled')
|
||||
.set('x-user-id', '123')
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getOpenFeatureClientToken, OpenFeatureModule, ServerProviderEvents } from '../src';
|
||||
import type { Client} from '@openfeature/server-sdk';
|
||||
import { OpenFeature } from '@openfeature/server-sdk';
|
||||
import { OpenFeature, OpenFeatureClient } from '@openfeature/server-sdk';
|
||||
import { getOpenFeatureDefaultTestModule } from './fixtures';
|
||||
|
||||
describe('OpenFeatureModule', () => {
|
||||
|
@ -33,19 +31,19 @@ describe('OpenFeatureModule', () => {
|
|||
|
||||
it('should return the SDKs default provider and not throw', async () => {
|
||||
expect(() => {
|
||||
moduleWithoutProvidersRef.get<Client>(getOpenFeatureClientToken());
|
||||
moduleWithoutProvidersRef.get<OpenFeatureClient>(getOpenFeatureClientToken());
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the default provider', async () => {
|
||||
const client = moduleRef.get<Client>(getOpenFeatureClientToken());
|
||||
const client = moduleRef.get<OpenFeatureClient>(getOpenFeatureClientToken());
|
||||
expect(client).toBeDefined();
|
||||
expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-default');
|
||||
});
|
||||
|
||||
it('should inject the client with the given scope', async () => {
|
||||
const client = moduleRef.get<Client>(getOpenFeatureClientToken('domainScopedClient'));
|
||||
const client = moduleRef.get<OpenFeatureClient>(getOpenFeatureClientToken('domainScopedClient'));
|
||||
expect(client).toBeDefined();
|
||||
expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-scoped');
|
||||
});
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
import { Controller, ForbiddenException, Get, Injectable, UseInterceptors } from '@nestjs/common';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs';
|
||||
import {
|
||||
BooleanFeatureFlag,
|
||||
ObjectFeatureFlag,
|
||||
NumberFeatureFlag,
|
||||
OpenFeatureClient,
|
||||
StringFeatureFlag,
|
||||
RequireFlagsEnabled,
|
||||
} from '../src';
|
||||
import type { Client, EvaluationDetails, FlagValue } from '@openfeature/server-sdk';
|
||||
import { Controller, Get, Injectable, UseInterceptors } from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { BooleanFeatureFlag, ObjectFeatureFlag, NumberFeatureFlag, FeatureClient, StringFeatureFlag } from '../src';
|
||||
import { OpenFeatureClient, EvaluationDetails, FlagValue } from '@openfeature/server-sdk';
|
||||
import { EvaluationContextInterceptor } from '../src';
|
||||
|
||||
@Injectable()
|
||||
export class OpenFeatureTestService {
|
||||
constructor(
|
||||
@OpenFeatureClient() public defaultClient: Client,
|
||||
@OpenFeatureClient({ domain: 'domainScopedClient' }) public domainScopedClient: Client,
|
||||
@FeatureClient() public defaultClient: OpenFeatureClient,
|
||||
@FeatureClient({ domain: 'domainScopedClient' }) public domainScopedClient: OpenFeatureClient,
|
||||
) {}
|
||||
|
||||
public async serviceMethod(flag: EvaluationDetails<FlagValue>) {
|
||||
|
@ -91,40 +83,11 @@ export class OpenFeatureController {
|
|||
public async handleDynamicContextInServiceRequest() {
|
||||
return this.testService.serviceMethodWithDynamicContext('testBooleanFlag');
|
||||
}
|
||||
|
||||
@RequireFlagsEnabled({
|
||||
flags: [{ flagKey: 'testBooleanFlag' }],
|
||||
})
|
||||
@Get('/flags-enabled')
|
||||
public async handleGuardedBooleanRequest() {
|
||||
return 'Get Boolean Flag Success!';
|
||||
}
|
||||
|
||||
@RequireFlagsEnabled({
|
||||
flags: [{ flagKey: 'testBooleanFlag' }],
|
||||
exception: new ForbiddenException(),
|
||||
})
|
||||
@Get('/flags-enabled-custom-exception')
|
||||
public async handleBooleanRequestWithCustomException() {
|
||||
return 'Get Boolean Flag Success!';
|
||||
}
|
||||
|
||||
@RequireFlagsEnabled({
|
||||
flags: [{ flagKey: 'testBooleanFlag2' }],
|
||||
exception: new ForbiddenException(),
|
||||
context: {
|
||||
targetingKey: 'user-id',
|
||||
},
|
||||
})
|
||||
@Get('/flags-enabled-custom-exception-with-context')
|
||||
public async handleBooleanRequestWithCustomExceptionAndContext() {
|
||||
return 'Get Boolean Flag Success!';
|
||||
}
|
||||
}
|
||||
|
||||
@Controller()
|
||||
@UseInterceptors(EvaluationContextInterceptor)
|
||||
export class OpenFeatureContextScopedController {
|
||||
export class OpenFeatureControllerContextScopedController {
|
||||
constructor(private testService: OpenFeatureTestService) {}
|
||||
|
||||
@Get('/controller-context')
|
||||
|
@ -137,27 +100,4 @@ export class OpenFeatureContextScopedController {
|
|||
) {
|
||||
return feature.pipe(map((details) => this.testService.serviceMethod(details)));
|
||||
}
|
||||
|
||||
@RequireFlagsEnabled({
|
||||
flags: [{ flagKey: 'testBooleanFlag' }],
|
||||
domain: 'domainScopedClient',
|
||||
})
|
||||
@Get('/controller-context/flags-enabled')
|
||||
public async handleBooleanRequest() {
|
||||
return 'Get Boolean Flag Success!';
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('require-flags-enabled')
|
||||
@RequireFlagsEnabled({
|
||||
flags: [{ flagKey: 'testBooleanFlag', defaultValue: false }, { flagKey: 'testBooleanFlag2' }],
|
||||
exception: new ForbiddenException(),
|
||||
})
|
||||
export class OpenFeatureRequireFlagsEnabledController {
|
||||
constructor() {}
|
||||
|
||||
@Get('/')
|
||||
public async handleGetRequest() {
|
||||
return 'Hello, world!';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
/* Language and Environment */
|
||||
"target": "ES2015",
|
||||
"target": "ES2022",
|
||||
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": [
|
||||
"ES2015"
|
||||
"ES2022"
|
||||
],
|
||||
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
|
@ -27,7 +27,7 @@
|
|||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
/* Modules */
|
||||
"module": "ES2015",
|
||||
"module": "ES2022",
|
||||
/* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node",
|
||||
|
|
|
@ -1,233 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## [1.0.1](https://github.com/open-feature/js-sdk/compare/react-sdk-v1.0.0...react-sdk-v1.0.1) (2025-08-18)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **react:** re-evaluate flags on re-render to detect silent provider … ([#1226](https://github.com/open-feature/js-sdk/issues/1226)) ([3105595](https://github.com/open-feature/js-sdk/commit/31055959265a53f52102590f54fa3168811ec678))
|
||||
|
||||
## [1.0.0](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.11...react-sdk-v1.0.0) (2025-04-14)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add polyfill for react use hook ([#1157](https://github.com/open-feature/js-sdk/issues/1157)) ([5afe61f](https://github.com/open-feature/js-sdk/commit/5afe61f9e351b037b04c93a1d81aee8016756748))
|
||||
* add support for abort controllers to event handlers ([#1151](https://github.com/open-feature/js-sdk/issues/1151)) ([6a22483](https://github.com/open-feature/js-sdk/commit/6a224830fa4e62fc30a7802536f6f6fc3f772038))
|
||||
|
||||
## [0.4.11](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.10...react-sdk-v0.4.11) (2025-02-07)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* export useOpenFeatureClientStatus hook ([#1082](https://github.com/open-feature/js-sdk/issues/1082)) ([4a6b860](https://github.com/open-feature/js-sdk/commit/4a6b8605444edeaf43355713357fecb97dd850b6))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* update sdk peer ([#1142](https://github.com/open-feature/js-sdk/issues/1142)) ([8bb6206](https://github.com/open-feature/js-sdk/commit/8bb620601e2b8dc7b62d717169b585bd1c886996))
|
||||
|
||||
## [0.4.10](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.9...react-sdk-v0.4.10) (2024-12-18)
|
||||
|
||||
|
||||
### 🔄 Refactoring
|
||||
|
||||
* export public option types ([#1101](https://github.com/open-feature/js-sdk/issues/1101)) ([16321c3](https://github.com/open-feature/js-sdk/commit/16321c31f27c5fce2c8e2adea893cf6e7e8ce3de))
|
||||
|
||||
## [0.4.9](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.8...react-sdk-v0.4.9) (2024-12-04)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* re-render if flagsChanged is falsy ([#1095](https://github.com/open-feature/js-sdk/issues/1095)) ([78516f4](https://github.com/open-feature/js-sdk/commit/78516f4181c82baf8c42fd64798fc2cfd8ff1056))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* fix typos, links, and format ([#1075](https://github.com/open-feature/js-sdk/issues/1075)) ([418409e](https://github.com/open-feature/js-sdk/commit/418409e3faafc6868a9f893267a4733db9931f93))
|
||||
|
||||
## [0.4.8](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.7...react-sdk-v0.4.8) (2024-10-29)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* bump minimum web peer ([#1072](https://github.com/open-feature/js-sdk/issues/1072)) ([eca8205](https://github.com/open-feature/js-sdk/commit/eca8205da7945395d19c09a4da67cd4c2d516227))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* add tracking sections ([#1068](https://github.com/open-feature/js-sdk/issues/1068)) ([e131faf](https://github.com/open-feature/js-sdk/commit/e131faffad9025e9c7194f39558bf3b3cec31807))
|
||||
|
||||
## [0.4.7](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.6...react-sdk-v0.4.7) (2024-10-29)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* avoid re-resolving flags unaffected by a change event ([#1024](https://github.com/open-feature/js-sdk/issues/1024)) ([b8f9b4e](https://github.com/open-feature/js-sdk/commit/b8f9b4ebaf4bdd93669fc6da09d9f97a498174d9))
|
||||
* implement tracking as per spec ([#1020](https://github.com/open-feature/js-sdk/issues/1020)) ([80f182e](https://github.com/open-feature/js-sdk/commit/80f182e1afbd3a705bf3de6a0d9886ccb3424b44))
|
||||
* use mutate context hook ([#1031](https://github.com/open-feature/js-sdk/issues/1031)) ([ec3d967](https://github.com/open-feature/js-sdk/commit/ec3d967f8b9dd0854706a904a5360f0a0b843595))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* add js docs for context mutator hook ([#1045](https://github.com/open-feature/js-sdk/issues/1045)) ([def3fe8](https://github.com/open-feature/js-sdk/commit/def3fe8dafc3d6ed3451a493e76842b7d2e8363c))
|
||||
* import type lint rule and fixes ([#1039](https://github.com/open-feature/js-sdk/issues/1039)) ([01fcb93](https://github.com/open-feature/js-sdk/commit/01fcb933d2cbd131a0f4a005173cdd1906087e18))
|
||||
|
||||
## [0.4.6](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.5...react-sdk-v0.4.6) (2024-09-23)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* failure to re-render on changes ([#1021](https://github.com/open-feature/js-sdk/issues/1021)) ([c927044](https://github.com/open-feature/js-sdk/commit/c927044c4934f0b8edfd2cdbbc0d60ad546b3dbc))
|
||||
|
||||
## [0.4.5](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.4...react-sdk-v0.4.5) (2024-09-04)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **react:** prevent rerenders when value is unchanged ([#987](https://github.com/open-feature/js-sdk/issues/987)) ([b7fc08e](https://github.com/open-feature/js-sdk/commit/b7fc08e27d225bdbf72c1985e7eef85adcd896b0))
|
||||
|
||||
## [0.4.4](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.3...react-sdk-v0.4.4) (2024-08-28)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* move client/ dir to web/ ([#991](https://github.com/open-feature/js-sdk/issues/991)) ([df4e72e](https://github.com/open-feature/js-sdk/commit/df4e72eabc3370801303470ca37263a0d4d9bb38))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* **react:** update the error message ([#978](https://github.com/open-feature/js-sdk/issues/978)) ([429c4ae](https://github.com/open-feature/js-sdk/commit/429c4ae941b66a1aa82b5aeea4bdb8b57bd05022))
|
||||
|
||||
## [0.4.3](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.2...react-sdk-v0.4.3) (2024-08-22)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* race condition in test provider with suspense ([#980](https://github.com/open-feature/js-sdk/issues/980)) ([0f187fe](https://github.com/open-feature/js-sdk/commit/0f187fe0b584e66b6283531eb7879c320967f921))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* fix flaky test timing ([ad46ade](https://github.com/open-feature/js-sdk/commit/ad46ade143b10366103d4ac199d728e8ae5ba7e8))
|
||||
|
||||
## [0.4.2](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.1...react-sdk-v0.4.2) (2024-07-29)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add test provider ([#971](https://github.com/open-feature/js-sdk/issues/971)) ([1c12d4d](https://github.com/open-feature/js-sdk/commit/1c12d4d548195bfc8c2f898a90ea97063aa8b3f7))
|
||||
|
||||
## [0.4.1](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.4.0...react-sdk-v0.4.1) (2024-06-11)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* lower compilation target to es2015 ([#957](https://github.com/open-feature/js-sdk/issues/957)) ([c2d6c17](https://github.com/open-feature/js-sdk/commit/c2d6c1761ae19f937deaff2f011a0380f8af7350))
|
||||
|
||||
## [0.4.0](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.4...react-sdk-v0.4.0) (2024-05-13)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* disable suspense by default, add suspense hooks ([#940](https://github.com/open-feature/js-sdk/issues/940))
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* disable suspense by default, add suspense hooks ([#940](https://github.com/open-feature/js-sdk/issues/940)) ([6bcef89](https://github.com/open-feature/js-sdk/commit/6bcef8977d0134c131af259dc0190a296e790382))
|
||||
* set context during provider init on web ([#919](https://github.com/open-feature/js-sdk/issues/919)) ([7e6c1c6](https://github.com/open-feature/js-sdk/commit/7e6c1c6e7082e75535bf81b4e70c8c57ef870b77))
|
||||
|
||||
## [0.3.4](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.3...react-sdk-v0.3.4) (2024-05-01)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* delayed suspense causes "flicker" ([#921](https://github.com/open-feature/js-sdk/issues/921)) ([4bce2a0](https://github.com/open-feature/js-sdk/commit/4bce2a0f1a5a716160b8862f1882d24c97688288))
|
||||
|
||||
## [0.3.3](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.2...react-sdk-v0.3.3) (2024-04-23)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* invocation hooks not called ([#916](https://github.com/open-feature/js-sdk/issues/916)) ([2f77738](https://github.com/open-feature/js-sdk/commit/2f7773809007733d1ccaeeaa58b1799d6c1731b4))
|
||||
|
||||
## [0.3.2](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.2-experimental...react-sdk-v0.3.2) (2024-04-18)
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* remove pre-release, update readme ([#908](https://github.com/open-feature/js-sdk/issues/908)) ([2532379](https://github.com/open-feature/js-sdk/commit/2532379f2ee5c38090a3e2c671edb2a6ca026bd5))
|
||||
|
||||
## [0.3.2-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.1-experimental...react-sdk-v0.3.2-experimental) (2024-04-11)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* re-render w/ useWhenProviderReady, add tests ([#901](https://github.com/open-feature/js-sdk/issues/901)) ([0f2094e](https://github.com/open-feature/js-sdk/commit/0f2094e2360ffed58a6103c00e5ba0ade6ac50eb))
|
||||
|
||||
## [0.3.1-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.3.0-experimental...react-sdk-v0.3.1-experimental) (2024-04-09)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* default options (re-renders not firing by default) ([#905](https://github.com/open-feature/js-sdk/issues/905)) ([a85e723](https://github.com/open-feature/js-sdk/commit/a85e72333fab85b3fcad87542c11fbed85ca9d85))
|
||||
|
||||
## [0.3.0-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.2.4-experimental...react-sdk-v0.3.0-experimental) (2024-04-08)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* options inheritance, useWhenProviderReady, suspend by default ([#900](https://github.com/open-feature/js-sdk/issues/900))
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* options inheritance, useWhenProviderReady, suspend by default ([#900](https://github.com/open-feature/js-sdk/issues/900)) ([539e741](https://github.com/open-feature/js-sdk/commit/539e7415de8dae333fed72ae80590021d9600830))
|
||||
|
||||
## [0.2.4-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.2.3-experimental...react-sdk-v0.2.4-experimental) (2024-04-03)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* query-style, generic useFlag hook ([#897](https://github.com/open-feature/js-sdk/issues/897)) ([5c17b8d](https://github.com/open-feature/js-sdk/commit/5c17b8dfcffd2f0145e5b2c79fa9dff842bbac92))
|
||||
|
||||
|
||||
### 🔄 Refactoring
|
||||
|
||||
* dir restructure ([#894](https://github.com/open-feature/js-sdk/issues/894)) ([ce9f65c](https://github.com/open-feature/js-sdk/commit/ce9f65c6ec41867f67c528997cf3acef367f9260))
|
||||
|
||||
## [0.2.3-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.2.2-experimental...react-sdk-v0.2.3-experimental) (2024-03-25)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* make domain/client optional ([#884](https://github.com/open-feature/js-sdk/issues/884)) ([2b633b5](https://github.com/open-feature/js-sdk/commit/2b633b56778dde9a8955f19ca207fa0e8dced884))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* prompt web-sdk 1.0 ([#871](https://github.com/open-feature/js-sdk/issues/871)) ([7d50d93](https://github.com/open-feature/js-sdk/commit/7d50d931d5cda349a31969c997e7581ea4883b6a))
|
||||
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* fix invalid link fragment ([9d63803](https://github.com/open-feature/js-sdk/commit/9d638038c0062704dc701bfbba3004e89ed59e3e))
|
||||
* remove emojis from react readme ([9e0e368](https://github.com/open-feature/js-sdk/commit/9e0e368d2328de2c7a4a5d91068aa75ecd70f8ed))
|
||||
|
||||
## [0.2.2-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.2.1-experimental...react-sdk-v0.2.2-experimental) (2024-03-06)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **types:** conflicts with peer types ([#852](https://github.com/open-feature/js-sdk/issues/852)) ([fdc8576](https://github.com/open-feature/js-sdk/commit/fdc8576f472253604e26c36e10c0d315f71dbe1c))
|
||||
|
||||
## [0.2.1-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.2.0-experimental...react-sdk-v0.2.1-experimental) (2024-03-05)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* maintain state in SDK, add RECONCILING ([#795](https://github.com/open-feature/js-sdk/issues/795)) ([cfb0a69](https://github.com/open-feature/js-sdk/commit/cfb0a69c42bd06bf59a7b8761fd90739872a8aeb))
|
||||
* suspend on RECONCILING, mem provider fixes ([#796](https://github.com/open-feature/js-sdk/issues/796)) ([8101ff1](https://github.com/open-feature/js-sdk/commit/8101ff197ff97808d14114e56aae27023f9b09f6))
|
||||
|
||||
## [0.2.0-experimental](https://github.com/open-feature/js-sdk/compare/react-sdk-v0.1.1-experimental...react-sdk-v0.2.0-experimental) (2024-02-27)
|
||||
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-start-version -->
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/react-sdk-v1.0.1">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.0.1&color=blue&style=for-the-badge" />
|
||||
<a href="https://github.com/open-feature/js-sdk/releases/tag/react-sdk-v0.2.0-experimental">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.2.0-experimental&color=blue&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-end -->
|
||||
<br/>
|
||||
|
@ -34,36 +34,24 @@
|
|||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
|
||||
🧪 This SDK is experimental.
|
||||
|
||||
## Overview
|
||||
|
||||
The OpenFeature React SDK adds React-specific functionality to the [OpenFeature Web SDK](https://openfeature.dev/docs/reference/technologies/client/web).
|
||||
|
||||
In addition to the feature provided by the [web sdk](https://openfeature.dev/docs/reference/technologies/client/web), capabilities include:
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick start](#quick-start)
|
||||
- [Requirements](#requirements)
|
||||
- [Install](#install)
|
||||
- [npm](#npm)
|
||||
- [yarn](#yarn)
|
||||
- [Required peer dependencies](#required-peer-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [OpenFeatureProvider context provider](#openfeatureprovider-context-provider)
|
||||
- [Evaluation hooks](#evaluation-hooks)
|
||||
- [Multiple Providers and Domains](#multiple-providers-and-domains)
|
||||
- [Re-rendering with Context Changes](#re-rendering-with-context-changes)
|
||||
- [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
|
||||
- [Suspense Support](#suspense-support)
|
||||
- [Tracking](#tracking)
|
||||
- [Testing](#testing)
|
||||
- [FAQ and troubleshooting](#faq-and-troubleshooting)
|
||||
- [Resources](#resources)
|
||||
- [Multiple Providers and domains](#multiple-providers-and-domains)
|
||||
- [Re-rendering with Context Changes](#re-rendering-with-context-changes)
|
||||
- [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
|
||||
- [Suspense Support](#suspense-support)
|
||||
|
||||
## Quick start
|
||||
## 🚀 Quick start
|
||||
|
||||
### Requirements
|
||||
|
||||
- ES2015-compatible web browser (Chrome, Edge, Firefox, etc)
|
||||
- ES2022-compatible web browser (Chrome, Edge, Firefox, etc)
|
||||
- React version 16.8+
|
||||
|
||||
### Install
|
||||
|
@ -74,31 +62,19 @@ In addition to the feature provided by the [web sdk](https://openfeature.dev/doc
|
|||
npm install --save @openfeature/react-sdk
|
||||
```
|
||||
|
||||
#### yarn
|
||||
|
||||
```sh
|
||||
# yarn requires manual installation of the peer dependencies (see below)
|
||||
yarn add @openfeature/react-sdk @openfeature/web-sdk @openfeature/core
|
||||
```
|
||||
|
||||
#### Required peer dependencies
|
||||
|
||||
The following list contains the peer dependencies of `@openfeature/react-sdk`.
|
||||
See the [package.json](./package.json) for the required versions.
|
||||
The following list contains the peer dependencies of `@openfeature/react-sdk` with its expected and compatible versions:
|
||||
|
||||
* `@openfeature/web-sdk`
|
||||
* `react`
|
||||
* `@openfeature/web-sdk`: >=0.4.10
|
||||
* `react`: >=16.8.0
|
||||
|
||||
### Usage
|
||||
|
||||
#### OpenFeatureProvider context provider
|
||||
|
||||
The `OpenFeatureProvider` is a [React context provider](https://react.dev/reference/react/createContext#provider) which represents a scope for feature flag evaluations within a React application.
|
||||
It binds an OpenFeature client to all evaluations within child components, and allows the use of evaluation hooks.
|
||||
The example below shows how to use the `OpenFeatureProvider` with OpenFeature's `InMemoryProvider`.
|
||||
|
||||
```tsx
|
||||
import { EvaluationContext, OpenFeatureProvider, useFlag, OpenFeature, InMemoryProvider } from '@openfeature/react-sdk';
|
||||
import { EvaluationContext, OpenFeatureProvider, useBooleanFlagValue, useBooleanFlagDetails, OpenFeature, InMemoryProvider } from '@openfeature/react-sdk';
|
||||
|
||||
const flagConfig = {
|
||||
'new-message': {
|
||||
|
@ -117,11 +93,8 @@ const flagConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
// Instantiate and set our provider (be sure this only happens once)!
|
||||
// Note: there's no need to await its initialization, the React SDK handles re-rendering and suspense for you!
|
||||
OpenFeature.setProvider(new InMemoryProvider(flagConfig));
|
||||
|
||||
// Enclose your content in the configured provider
|
||||
function App() {
|
||||
return (
|
||||
<OpenFeatureProvider>
|
||||
|
@ -129,39 +102,26 @@ function App() {
|
|||
</OpenFeatureProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Evaluation hooks
|
||||
|
||||
Within the provider, you can use the various evaluation hooks to evaluate flags.
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
// Use the "query-style" flag evaluation hook, specifying a flag-key and a default value.
|
||||
const { value: showNewMessage } = useFlag('new-message', true);
|
||||
const newMessage = useBooleanFlagValue('new-message', false);
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
|
||||
{newMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
You can use the strongly typed flag value and flag evaluation detail hooks as well if you prefer.
|
||||
|
||||
```tsx
|
||||
import { useBooleanFlagValue } from '@openfeature/react-sdk';
|
||||
|
||||
// boolean flag evaluation
|
||||
const value = useBooleanFlagValue('new-message', false);
|
||||
```
|
||||
You use the detailed flag evaluation hooks to evaluate the flag and get additional information about the flag and the evaluation.
|
||||
|
||||
```tsx
|
||||
import { useBooleanFlagDetails } from '@openfeature/react-sdk';
|
||||
|
||||
// "detailed" boolean flag evaluation
|
||||
const {
|
||||
value,
|
||||
variant,
|
||||
|
@ -170,12 +130,13 @@ const {
|
|||
} = useBooleanFlagDetails('new-message', false);
|
||||
```
|
||||
|
||||
#### Multiple Providers and Domains
|
||||
### Multiple Providers and Domains
|
||||
|
||||
|
||||
Multiple providers can be used by passing a `domain` to the `OpenFeatureProvider`:
|
||||
|
||||
```tsx
|
||||
// Flags within this domain will use the client/provider associated with `my-domain`,
|
||||
// Flags within this domain will use the a client/provider associated with `my-domain`,
|
||||
function App() {
|
||||
return (
|
||||
<OpenFeatureProvider domain={'my-domain'}>
|
||||
|
@ -191,63 +152,47 @@ This is analogous to:
|
|||
OpenFeature.getClient('my-domain');
|
||||
```
|
||||
|
||||
For more information about `domains`, refer to the [web SDK](https://github.com/open-feature/js-sdk/blob/main/packages/web/README.md).
|
||||
For more information about `domains`, refer to the [web SDK](https://github.com/open-feature/js-sdk/blob/main/packages/client/README.md).
|
||||
|
||||
#### Re-rendering with Context Changes
|
||||
### Re-rendering with Context Changes
|
||||
|
||||
By default, if the OpenFeature [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) is modified, components will be re-rendered.
|
||||
This is useful in cases where flag values are dependant on user-attributes or other application state (user logged in, items in card, etc).
|
||||
You can disable this feature in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)):
|
||||
You can disable this feature in the hook options:
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
const { value: showNewMessage } = useFlag('new-message', false, { updateOnContextChanged: false });
|
||||
const newMessage = useBooleanFlagValue('new-message', false, { updateOnContextChanged: false });
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
<MyComponents></MyComponents>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For more information about how evaluation context works in the React SDK, see the documentation on OpenFeature's [static context SDK paradigm](https://openfeature.dev/specification/glossary/#static-context-paradigm).
|
||||
|
||||
#### Re-rendering with Flag Configuration Changes
|
||||
### Re-rendering with Flag Configuration Changes
|
||||
|
||||
By default, if the underlying provider emits a `ConfigurationChanged` event, components will be re-rendered.
|
||||
This is useful if you want your UI to immediately reflect changes in the backend flag configuration.
|
||||
You can disable this feature in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)):
|
||||
You can disable this feature in the hook options:
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
const { value: showNewMessage } = useFlag('new-message', false, { updateOnConfigurationChanged: false });
|
||||
const newMessage = useBooleanFlagValue('new-message', false, { updateOnConfigurationChanged: false });
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
{showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
<MyComponents></MyComponents>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
If your provider doesn't support updates, this configuration has no impact.
|
||||
Note that if your provider doesn't support updates, this configuration has no impact.
|
||||
|
||||
> [!NOTE]
|
||||
> If your provider includes a list of [flags changed](https://open-feature.github.io/js-sdk/types/_openfeature_server_sdk.ConfigChangeEvent.html) in its `PROVIDER_CONFIGURATION_CHANGED` event, that list of flags is used to decide which flag evaluation hooks should re-run by diffing the latest value of these flags with the previous render.
|
||||
> If your provider event does not the include the `flags changed` list, then the SDK diffs all flags with the previous render to determine which hooks should re-run.
|
||||
|
||||
#### Suspense Support
|
||||
|
||||
> [!NOTE]
|
||||
> React suspense is an experimental feature and is subject to change in future versions.
|
||||
### Suspense Support
|
||||
|
||||
Frequently, providers need to perform some initial startup tasks.
|
||||
It may be desirable not to display components with feature flags until this is complete or when the context changes.
|
||||
Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy.
|
||||
Use `useSuspenseFlag` or pass `{ suspend: true }` in the hook options to leverage this functionality.
|
||||
It may be desireable not to display components with feature flags until this is complete.
|
||||
Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy:
|
||||
|
||||
```tsx
|
||||
function Content() {
|
||||
|
@ -260,12 +205,12 @@ function Content() {
|
|||
}
|
||||
|
||||
function Message() {
|
||||
// component to render after READY, equivalent to useFlag('new-message', false, { suspend: true });
|
||||
const { value: showNewMessage } = useSuspenseFlag('new-message', false);
|
||||
// component to render after READY.
|
||||
const newMessage = useBooleanFlagValue('new-message', false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNewMessage ? (
|
||||
{newMessage ? (
|
||||
<p>Welcome to this OpenFeature-enabled React app!</p>
|
||||
) : (
|
||||
<p>Welcome to this plain old React app!</p>
|
||||
|
@ -280,123 +225,6 @@ function Fallback() {
|
|||
}
|
||||
```
|
||||
|
||||
This can be disabled in the hook options (or in the [OpenFeatureProvider](#openfeatureprovider-context-provider)).
|
||||
|
||||
#### Tracking
|
||||
|
||||
The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations.
|
||||
This is essential for robust experimentation powered by feature flags.
|
||||
For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a [hook](https://openfeature.dev/docs/reference/technologies/client/web/#hooks) or [provider](https://openfeature.dev/docs/reference/technologies/client/web/#providers) can be associated with telemetry reported in the client's `track` function.
|
||||
|
||||
The React SDK includes a hook for firing tracking events in the `<OpenFeatureProvider>` context in use:
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
// get a tracking function for this <OpenFeatureProvider>.
|
||||
const { track } = useTrack();
|
||||
|
||||
// call the tracking event
|
||||
// can be done in render, useEffect, or in handlers, depending on your use case
|
||||
track(eventName, trackingDetails);
|
||||
|
||||
return <>...</>;
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
The React SDK includes a built-in context provider for testing.
|
||||
This allows you to easily test components that use evaluation hooks, such as `useFlag`.
|
||||
If you try to test a component (in this case, `MyComponent`) which uses an evaluation hook, you might see an error message like:
|
||||
|
||||
> No OpenFeature client available - components using OpenFeature must be wrapped with an `<OpenFeatureProvider>`.
|
||||
|
||||
You can resolve this by simply wrapping your component under test in the OpenFeatureTestProvider:
|
||||
|
||||
```tsx
|
||||
// use default values for all evaluations
|
||||
<OpenFeatureTestProvider>
|
||||
<MyComponent />
|
||||
</OpenFeatureTestProvider>
|
||||
```
|
||||
|
||||
The basic configuration above will simply use the default value provided in code.
|
||||
If you'd like to control the values returned by the evaluation hooks, you can pass a map of flag keys and values:
|
||||
|
||||
```tsx
|
||||
// return `true` for all evaluations of `'my-boolean-flag'`
|
||||
<OpenFeatureTestProvider flagValueMap={{ 'my-boolean-flag': true }}>
|
||||
<MyComponent />
|
||||
</OpenFeatureTestProvider>
|
||||
```
|
||||
|
||||
Additionally, you can pass an artificial delay for the provider startup to test your suspense boundaries or loaders/spinners impacted by feature flags:
|
||||
|
||||
```tsx
|
||||
// delay the provider start by 1000ms and then return `true` for all evaluations of `'my-boolean-flag'`
|
||||
<OpenFeatureTestProvider delayMs={1000} flagValueMap={{ 'my-boolean-flag': true }}>
|
||||
<MyComponent />
|
||||
</OpenFeatureTestProvider>
|
||||
```
|
||||
|
||||
For maximum control, you can also pass your own mock provider implementation.
|
||||
The type of this option is `Partial<Provider>`, so you can pass an incomplete implementation:
|
||||
|
||||
```tsx
|
||||
class MyTestProvider implements Partial<Provider> {
|
||||
// implement the relevant resolver
|
||||
resolveBooleanEvaluation(): ResolutionDetails<boolean> {
|
||||
return {
|
||||
value: true,
|
||||
variant: 'my-variant',
|
||||
reason: 'MY_REASON',
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// use your custom testing provider
|
||||
<OpenFeatureTestProvider provider={new MyTestProvider()}>
|
||||
<MyComponent />
|
||||
</OpenFeatureTestProvider>,
|
||||
```
|
||||
|
||||
## FAQ and troubleshooting
|
||||
|
||||
> I get an error that says something like: `A React component suspended while rendering, but no fallback UI was specified.`
|
||||
|
||||
The OpenFeature React SDK features built-in [suspense support](#suspense-support).
|
||||
This means that it will render your loading fallback automatically while your provider starts up and during context reconciliation for any of your components using feature flags!
|
||||
If you use suspense and neglect to create a suspense boundary around any components using feature flags, you will see this error.
|
||||
Add a suspense boundary to resolve this issue.
|
||||
Alternatively, you can disable this suspense (the default) by removing `suspendWhileReconciling=true`, `suspendUntilReady=true` or `suspend=true` in the [evaluation hooks](#evaluation-hooks) or the [OpenFeatureProvider](#openfeatureprovider-context-provider) (which applies to all evaluation hooks in child components).
|
||||
|
||||
> I get odd rendering issues or errors when components mount if I use the suspense features.
|
||||
|
||||
In React 16/17's "Legacy Suspense", when a component suspends, its sibling components initially mount and then are hidden.
|
||||
This can cause surprising effects and inconsistencies if sibling components are rendered while the provider is still getting ready.
|
||||
To fix this, you can upgrade to React 18, which uses "Concurrent Suspense", in which siblings are not mounted until their suspended sibling resolves.
|
||||
Alternatively, if you cannot upgrade to React 18, you can use the `useWhenProviderReady` utility hook in any sibling components to prevent them from mounting until the provider is ready.
|
||||
|
||||
> I am using multiple `OpenFeatureProvider` contexts, but they share the same provider or evaluation context. Why?
|
||||
|
||||
The `OpenFeatureProvider` binds a `client` to all child components, but the provider and context associated with that client is controlled by the `domain` parameter.
|
||||
This is consistent with all OpenFeature SDKs.
|
||||
To scope an OpenFeatureProvider to a particular provider/context, set the `domain` parameter on your `OpenFeatureProvider`:
|
||||
|
||||
```tsx
|
||||
<OpenFeatureProvider domain={'my-domain'}>
|
||||
<Page></Page>
|
||||
</OpenFeatureProvider>
|
||||
```
|
||||
|
||||
> I can import things form the `@openfeature/react-sdk`, `@openfeature/web-sdk`, and `@openfeature/core`; which should I use?
|
||||
|
||||
The `@openfeature/react-sdk` re-exports everything from its peers (`@openfeature/web-sdk` and `@openfeature/core`) and adds the React-specific features.
|
||||
You can import everything from the `@openfeature/react-sdk` directly.
|
||||
Avoid importing anything from `@openfeature/web-sdk` or `@openfeature/core`.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Example repo](https://github.com/open-feature/react-test-app)
|
||||
- [Example repo](https://github.com/open-feature/react-test-app)
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openfeature/react-sdk",
|
||||
"version": "1.0.1",
|
||||
"version": "0.2.0-experimental",
|
||||
"description": "OpenFeature React SDK",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"files": [
|
||||
|
@ -16,10 +16,9 @@
|
|||
"scripts": {
|
||||
"test": "jest --verbose",
|
||||
"lint": "eslint ./",
|
||||
"lint:fix": "eslint ./ --fix",
|
||||
"clean": "shx rm -rf ./dist",
|
||||
"build:react-esm": "esbuild src/index.ts --bundle --external:react --external:@openfeature/web-sdk --sourcemap --target=es2015 --platform=browser --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:react-cjs": "esbuild src/index.ts --bundle --external:react --external:@openfeature/web-sdk --sourcemap --target=es2015 --platform=browser --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:react-esm": "esbuild src/index.ts --bundle --external:react --external:@openfeature/web-sdk --sourcemap --target=es2022 --platform=browser --format=esm --outfile=./dist/esm/index.js --analyze",
|
||||
"build:react-cjs": "esbuild src/index.ts --bundle --external:react --external:@openfeature/web-sdk --sourcemap --target=es2022 --platform=browser --format=cjs --outfile=./dist/cjs/index.js --analyze",
|
||||
"build:rollup-types": "rollup -c ../../rollup.config.mjs",
|
||||
"build": "npm run clean && npm run build:react-esm && npm run build:react-cjs && npm run build:rollup-types",
|
||||
"postbuild": "shx cp ./../../package.esm.json ./dist/esm/package.json",
|
||||
|
@ -47,7 +46,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/open-feature/js-sdk#readme",
|
||||
"peerDependencies": {
|
||||
"@openfeature/web-sdk": "^1.5.0",
|
||||
"@openfeature/web-sdk": ">=0.4.14",
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './use-context-mutator';
|
|
@ -1,51 +0,0 @@
|
|||
import { useCallback, useContext, useRef } from 'react';
|
||||
import type { EvaluationContext } from '@openfeature/web-sdk';
|
||||
import { OpenFeature } from '@openfeature/web-sdk';
|
||||
import { Context } from '../internal';
|
||||
|
||||
export type ContextMutationOptions = {
|
||||
/**
|
||||
* Mutate the default context instead of the domain scoped context applied at the `<OpenFeatureProvider/>`.
|
||||
* Note, if the `<OpenFeatureProvider/>` has no domain specified, the default is used.
|
||||
* See the {@link https://openfeature.dev/docs/reference/technologies/client/web/#manage-evaluation-context-for-domains|documentation} for more information.
|
||||
* @default false
|
||||
*/
|
||||
defaultContext?: boolean;
|
||||
};
|
||||
|
||||
export type ContextMutation = {
|
||||
/**
|
||||
* Context-aware function to set the desired context (see: {@link ContextMutationOptions} for details).
|
||||
* There's generally no need to await the result of this function; flag evaluation hooks will re-render when the context is updated.
|
||||
* This promise never rejects.
|
||||
* @param updatedContext
|
||||
* @returns Promise for awaiting the context update
|
||||
*/
|
||||
setContext: (updatedContext: EvaluationContext) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get context-aware tracking function(s) for mutating the evaluation context associated with this domain, or the default context if `defaultContext: true`.
|
||||
* See the {@link https://openfeature.dev/docs/reference/technologies/client/web/#targeting-and-context|documentation} for more information.
|
||||
* @param {ContextMutationOptions} options options for the generated function
|
||||
* @returns {ContextMutation} context-aware function(s) to mutate evaluation context
|
||||
*/
|
||||
export function useContextMutator(options: ContextMutationOptions = { defaultContext: false }): ContextMutation {
|
||||
const { domain } = useContext(Context) || {};
|
||||
const previousContext = useRef<null | EvaluationContext>(null);
|
||||
|
||||
const setContext = useCallback(async (updatedContext: EvaluationContext) => {
|
||||
if (previousContext.current !== updatedContext) {
|
||||
if (!domain || options?.defaultContext) {
|
||||
OpenFeature.setContext(updatedContext);
|
||||
} else {
|
||||
OpenFeature.setContext(domain, updatedContext);
|
||||
}
|
||||
previousContext.current = updatedContext;
|
||||
}
|
||||
}, [domain]);
|
||||
|
||||
return {
|
||||
setContext,
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './use-feature-flag';
|
|
@ -1,377 +0,0 @@
|
|||
import type {
|
||||
Client,
|
||||
ClientProviderEvents,
|
||||
EvaluationDetails,
|
||||
EventHandler,
|
||||
FlagEvaluationOptions,
|
||||
FlagValue,
|
||||
JsonValue,
|
||||
} from '@openfeature/web-sdk';
|
||||
import { ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
DEFAULT_OPTIONS,
|
||||
isEqual,
|
||||
normalizeOptions,
|
||||
suspendUntilInitialized,
|
||||
suspendUntilReconciled,
|
||||
useProviderOptions,
|
||||
} from '../internal';
|
||||
import type { ReactFlagEvaluationNoSuspenseOptions, ReactFlagEvaluationOptions } from '../options';
|
||||
import { useOpenFeatureClient } from '../provider/use-open-feature-client';
|
||||
import { useOpenFeatureClientStatus } from '../provider/use-open-feature-client-status';
|
||||
import { useOpenFeatureProvider } from '../provider/use-open-feature-provider';
|
||||
import type { FlagQuery } from '../query';
|
||||
import { HookFlagQuery } from '../internal/hook-flag-query';
|
||||
|
||||
// This type is a bit wild-looking, but I think we need it.
|
||||
// We have to use the conditional, because otherwise useFlag('key', false) would return false, not boolean (too constrained).
|
||||
// We have a duplicate for the hook return below, this one is just used for casting because the name isn't as clear
|
||||
type ConstrainedFlagQuery<T> = FlagQuery<
|
||||
T extends boolean
|
||||
? boolean
|
||||
: T extends number
|
||||
? number
|
||||
: T extends string
|
||||
? string
|
||||
: T extends JsonValue
|
||||
? T
|
||||
: JsonValue
|
||||
>;
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag generically, returning an react-flavored queryable object.
|
||||
* The resolver method to use is based on the type of the defaultValue.
|
||||
* For type-specific hooks, use {@link useBooleanFlagValue}, {@link useBooleanFlagDetails} and equivalents.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {FlagValue} T A optional generic argument constraining the default.
|
||||
* @param {T} defaultValue the default value; used to determine what resolved type should be used.
|
||||
* @param {ReactFlagEvaluationOptions} options for this evaluation
|
||||
* @returns { FlagQuery } a queryable object containing useful information about the flag.
|
||||
*/
|
||||
export function useFlag<T extends FlagValue = FlagValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): FlagQuery<
|
||||
T extends boolean
|
||||
? boolean
|
||||
: T extends number
|
||||
? number
|
||||
: T extends string
|
||||
? string
|
||||
: T extends JsonValue
|
||||
? T
|
||||
: JsonValue
|
||||
> {
|
||||
// use the default value to determine the resolver to call
|
||||
const query =
|
||||
typeof defaultValue === 'boolean'
|
||||
? new HookFlagQuery<boolean>(useBooleanFlagDetails(flagKey, defaultValue, options))
|
||||
: typeof defaultValue === 'number'
|
||||
? new HookFlagQuery<number>(useNumberFlagDetails(flagKey, defaultValue, options))
|
||||
: typeof defaultValue === 'string'
|
||||
? new HookFlagQuery<string>(useStringFlagDetails(flagKey, defaultValue, options))
|
||||
: new HookFlagQuery<JsonValue>(useObjectFlagDetails(flagKey, defaultValue, options));
|
||||
// TS sees this as HookFlagQuery<JsonValue>, because the compiler isn't aware of the `typeof` checks above.
|
||||
return query as unknown as ConstrainedFlagQuery<T>;
|
||||
}
|
||||
|
||||
// alias to the return value of useFlag, used to keep useSuspenseFlag consistent
|
||||
type UseFlagReturn<T extends FlagValue> = ReturnType<typeof useFlag<T>>;
|
||||
|
||||
/**
|
||||
* Equivalent to {@link useFlag} with `options: { suspend: true }`
|
||||
* @experimental Suspense is an experimental feature subject to change in future versions.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {FlagValue} T A optional generic argument constraining the default.
|
||||
* @param {T} defaultValue the default value; used to determine what resolved type should be used.
|
||||
* @param {ReactFlagEvaluationNoSuspenseOptions} options for this evaluation
|
||||
* @returns { UseFlagReturn<T> } a queryable object containing useful information about the flag.
|
||||
*/
|
||||
export function useSuspenseFlag<T extends FlagValue = FlagValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationNoSuspenseOptions,
|
||||
): UseFlagReturn<T> {
|
||||
return useFlag(flagKey, defaultValue, { ...options, suspendUntilReady: true, suspendWhileReconciling: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning a boolean.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @param {boolean} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { boolean} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useBooleanFlagValue(
|
||||
flagKey: string,
|
||||
defaultValue: boolean,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): boolean {
|
||||
return useBooleanFlagDetails(flagKey, defaultValue, options).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning evaluation details.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @param {boolean} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { EvaluationDetails<boolean>} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useBooleanFlagDetails(
|
||||
flagKey: string,
|
||||
defaultValue: boolean,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): EvaluationDetails<boolean> {
|
||||
return attachHandlersAndResolve(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
(client) => {
|
||||
return client.getBooleanDetails;
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning a string.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {string} [T=string] A optional generic argument constraining the string
|
||||
* @param {T} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { boolean} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useStringFlagValue<T extends string = string>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): string {
|
||||
return useStringFlagDetails(flagKey, defaultValue, options).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning evaluation details.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {string} [T=string] A optional generic argument constraining the string
|
||||
* @param {T} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { EvaluationDetails<string>} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useStringFlagDetails<T extends string = string>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): EvaluationDetails<string> {
|
||||
return attachHandlersAndResolve(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
(client) => {
|
||||
return client.getStringDetails<T>;
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning a number.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {number} [T=number] A optional generic argument constraining the number
|
||||
* @param {T} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { boolean} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useNumberFlagValue<T extends number = number>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): number {
|
||||
return useNumberFlagDetails(flagKey, defaultValue, options).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning evaluation details.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {number} [T=number] A optional generic argument constraining the number
|
||||
* @param {T} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { EvaluationDetails<number>} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useNumberFlagDetails<T extends number = number>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): EvaluationDetails<number> {
|
||||
return attachHandlersAndResolve(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
(client) => {
|
||||
return client.getNumberDetails<T>;
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning an object.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
|
||||
* @param {T} defaultValue the default value
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { boolean} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useObjectFlagValue<T extends JsonValue = JsonValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): T {
|
||||
return useObjectFlagDetails<T>(flagKey, defaultValue, options).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature flag, returning evaluation details.
|
||||
* By default, components will re-render when the flag value changes.
|
||||
* For a generic hook returning a queryable interface, see {@link useFlag}.
|
||||
* @param {string} flagKey the flag identifier
|
||||
* @param {T} defaultValue the default value
|
||||
* @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
|
||||
* @param {ReactFlagEvaluationOptions} options options for this evaluation
|
||||
* @returns { EvaluationDetails<T>} a EvaluationDetails object for this evaluation
|
||||
*/
|
||||
export function useObjectFlagDetails<T extends JsonValue = JsonValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): EvaluationDetails<T> {
|
||||
return attachHandlersAndResolve(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
(client) => {
|
||||
return client.getObjectDetails<T>;
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
// determines if a flag should be re-evaluated based on a list of changed flags
|
||||
function shouldEvaluateFlag(flagKey: string, flagsChanged?: string[]): boolean {
|
||||
// if flagsChange is missing entirely, we don't know what to re-render
|
||||
return !flagsChanged || flagsChanged.includes(flagKey);
|
||||
}
|
||||
|
||||
function attachHandlersAndResolve<T extends FlagValue>(
|
||||
flagKey: string,
|
||||
defaultValue: T,
|
||||
resolver: (
|
||||
client: Client,
|
||||
) => (flagKey: string, defaultValue: T, options?: FlagEvaluationOptions) => EvaluationDetails<T>,
|
||||
options?: ReactFlagEvaluationOptions,
|
||||
): EvaluationDetails<T> {
|
||||
// highest priority > evaluation hook options > provider options > default options > lowest priority
|
||||
const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options) };
|
||||
const client = useOpenFeatureClient();
|
||||
const status = useOpenFeatureClientStatus();
|
||||
const provider = useOpenFeatureProvider();
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
if (defaultedOptions.suspendUntilReady && status === ProviderStatus.NOT_READY) {
|
||||
suspendUntilInitialized(provider, client);
|
||||
}
|
||||
|
||||
if (defaultedOptions.suspendWhileReconciling && status === ProviderStatus.RECONCILING) {
|
||||
suspendUntilReconciled(client);
|
||||
}
|
||||
|
||||
const [evaluationDetails, setEvaluationDetails] = useState<EvaluationDetails<T>>(() =>
|
||||
resolver(client).call(client, flagKey, defaultValue, options),
|
||||
);
|
||||
|
||||
// Re-evaluate when dependencies change (handles prop changes like flagKey), or if during a re-render, we have detected a change in the evaluated value
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const newDetails = resolver(client).call(client, flagKey, defaultValue, options);
|
||||
if (!isEqual(newDetails.value, evaluationDetails.value)) {
|
||||
setEvaluationDetails(newDetails);
|
||||
}
|
||||
}, [client, flagKey, defaultValue, options, resolver, evaluationDetails]);
|
||||
|
||||
// Maintain a mutable reference to the evaluation details to have a up-to-date reference in the handlers.
|
||||
const evaluationDetailsRef = useRef<EvaluationDetails<T>>(evaluationDetails);
|
||||
useEffect(() => {
|
||||
evaluationDetailsRef.current = evaluationDetails;
|
||||
}, [evaluationDetails]);
|
||||
|
||||
const updateEvaluationDetailsCallback = useCallback(() => {
|
||||
const updatedEvaluationDetails = resolver(client).call(client, flagKey, defaultValue, options);
|
||||
|
||||
/**
|
||||
* Avoid re-rendering if the value hasn't changed. We could expose a means
|
||||
* to define a custom comparison function if users require a more
|
||||
* sophisticated comparison in the future.
|
||||
*/
|
||||
if (!isEqual(updatedEvaluationDetails.value, evaluationDetailsRef.current.value)) {
|
||||
setEvaluationDetails(updatedEvaluationDetails);
|
||||
}
|
||||
}, [client, flagKey, defaultValue, options, resolver]);
|
||||
|
||||
const configurationChangeCallback = useCallback<EventHandler<ClientProviderEvents.ConfigurationChanged>>(
|
||||
(eventDetails) => {
|
||||
if (shouldEvaluateFlag(flagKey, eventDetails?.flagsChanged)) {
|
||||
updateEvaluationDetailsCallback();
|
||||
}
|
||||
},
|
||||
[flagKey, updateEvaluationDetailsCallback],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
if (status === ProviderStatus.NOT_READY) {
|
||||
// update when the provider is ready
|
||||
client.addHandler(ProviderEvents.Ready, updateEvaluationDetailsCallback, { signal: controller.signal });
|
||||
}
|
||||
|
||||
if (defaultedOptions.updateOnContextChanged) {
|
||||
// update when the context changes
|
||||
client.addHandler(ProviderEvents.ContextChanged, updateEvaluationDetailsCallback, { signal: controller.signal });
|
||||
}
|
||||
|
||||
if (defaultedOptions.updateOnConfigurationChanged) {
|
||||
// update when the provider configuration changes
|
||||
client.addHandler(ProviderEvents.ConfigurationChanged, configurationChangeCallback, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
// cleanup the handlers
|
||||
controller.abort();
|
||||
};
|
||||
}, [
|
||||
client,
|
||||
status,
|
||||
defaultedOptions.updateOnContextChanged,
|
||||
defaultedOptions.updateOnConfigurationChanged,
|
||||
updateEvaluationDetailsCallback,
|
||||
configurationChangeCallback,
|
||||
]);
|
||||
|
||||
return evaluationDetails;
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
export * from './evaluation';
|
||||
export * from './query';
|
||||
export * from './use-feature-flag';
|
||||
export * from './provider';
|
||||
export * from './context';
|
||||
export * from './tracking';
|
||||
export * from './options';
|
||||
// re-export the web-sdk so consumers can access that API from the react-sdk
|
||||
export * from '@openfeature/web-sdk';
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import type { Client } from '@openfeature/web-sdk';
|
||||
import React from 'react';
|
||||
import type { NormalizedOptions, ReactFlagEvaluationOptions } from '../options';
|
||||
import { normalizeOptions } from '.';
|
||||
|
||||
/**
|
||||
* The underlying React context.
|
||||
*
|
||||
* **DO NOT EXPORT PUBLICLY**
|
||||
* @internal
|
||||
*/
|
||||
export const Context = React.createContext<
|
||||
{ client: Client; domain?: string; options: ReactFlagEvaluationOptions } | undefined
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Get a normalized copy of the options used for this OpenFeatureProvider, see {@link normalizeOptions}.
|
||||
*
|
||||
* **DO NOT EXPORT PUBLICLY**
|
||||
* @internal
|
||||
* @returns {NormalizedOptions} normalized options the defaulted options, not defaulted or normalized.
|
||||
*/
|
||||
export function useProviderOptions(): NormalizedOptions {
|
||||
const { options } = React.useContext(Context) || {};
|
||||
return normalizeOptions(options);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
const context = 'Components using OpenFeature must be wrapped with an <OpenFeatureProvider>.';
|
||||
const tip = 'If you are seeing this in a test, see: https://openfeature.dev/docs/reference/technologies/client/web/react#testing';
|
||||
|
||||
export class MissingContextError extends Error {
|
||||
constructor(reason: string) {
|
||||
super(`${reason}: ${context} ${tip}`);
|
||||
this.name = 'MissingContextError';
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue