@cqrs-ddd/pipeline-feature-flags 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +35 -2
- package/dist/errors/feature-disabled.error.d.ts +15 -0
- package/dist/errors/feature-disabled.error.d.ts.map +1 -0
- package/dist/errors/feature-disabled.error.js +21 -0
- package/dist/errors/feature-disabled.error.js.map +1 -0
- package/dist/errors/feature-flag-evaluation.error.d.ts +17 -0
- package/dist/errors/feature-flag-evaluation.error.d.ts.map +1 -0
- package/dist/errors/feature-flag-evaluation.error.js +24 -0
- package/dist/errors/feature-flag-evaluation.error.js.map +1 -0
- package/dist/feature-flag.behavior.d.ts +148 -0
- package/dist/feature-flag.behavior.d.ts.map +1 -0
- package/dist/feature-flag.behavior.js +261 -0
- package/dist/feature-flag.behavior.js.map +1 -0
- package/dist/helpers/build-attributes.d.ts +48 -0
- package/dist/helpers/build-attributes.d.ts.map +1 -0
- package/dist/helpers/build-attributes.js +63 -0
- package/dist/helpers/build-attributes.js.map +1 -0
- package/dist/helpers/evaluation-context.d.ts +17 -0
- package/dist/helpers/evaluation-context.d.ts.map +1 -0
- package/dist/helpers/evaluation-context.js +33 -0
- package/dist/helpers/evaluation-context.js.map +1 -0
- package/dist/helpers/feature-flag-client.d.ts +27 -0
- package/dist/helpers/feature-flag-client.d.ts.map +1 -0
- package/dist/helpers/feature-flag-client.js +57 -0
- package/dist/helpers/feature-flag-client.js.map +1 -0
- package/dist/helpers/feature-flag.intent.d.ts +22 -0
- package/dist/helpers/feature-flag.intent.d.ts.map +1 -0
- package/dist/helpers/feature-flag.intent.js +20 -0
- package/dist/helpers/feature-flag.intent.js.map +1 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +31 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/feature-flags-options.interface.d.ts +154 -0
- package/dist/interfaces/feature-flags-options.interface.d.ts.map +1 -0
- package/dist/interfaces/feature-flags-options.interface.js +3 -0
- package/dist/interfaces/feature-flags-options.interface.js.map +1 -0
- package/package.json +67 -3
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cqrs-ddd/pipeline-feature-flags
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Feature flags for `@cqrs-ddd/pipeline` through [OpenFeature](https://openfeature.dev): an
|
|
4
|
+
operation runs only while its boolean flag is on, with any OpenFeature provider
|
|
5
|
+
(Unleash, Flagsmith, LaunchDarkly, a file). `createFeatureFlagClient()` registers the
|
|
6
|
+
provider and resolves the client; `toHttpResponse()` from
|
|
7
|
+
`@cqrs-ddd/pipeline-feature-flags/http` turns a disabled feature into a 403, or a 404 that
|
|
8
|
+
hides it.
|
|
9
|
+
|
|
10
|
+
**Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-feature-flags/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-feature-flags/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @cqrs-ddd/pipeline-feature-flags @cqrs-ddd/pipeline @openfeature/server-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
|
|
19
|
+
it with `require()`.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const client = await createFeatureFlagClient({ provider: new UnleashProvider(config) });
|
|
25
|
+
const pipeline = createPipeline({ behaviors: [new FeatureFlagBehavior(client)] });
|
|
26
|
+
|
|
27
|
+
export const checkout = pipeline.wrap(
|
|
28
|
+
{ name: 'checkout', kind: 'command' },
|
|
29
|
+
featureFlag({ flag: 'new-checkout' }),
|
|
30
|
+
)(async (cart: Cart) => orders.place(cart));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
|
|
36
|
+
`COMMERCIAL_LICENSE.txt`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link FeatureFlagBehavior} when a handler is gated behind a flag
|
|
3
|
+
* that resolves to disabled and no `fallback` was configured.
|
|
4
|
+
*
|
|
5
|
+
* Over HTTP, register {@link FeatureDisabledFilter}: it answers `403 Forbidden`
|
|
6
|
+
* with the flag, or a plain `404 Not Found` to hide the feature.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FeatureDisabledError extends Error {
|
|
9
|
+
/** The flag key that gated the request. */
|
|
10
|
+
readonly flag: string;
|
|
11
|
+
/** The request that was blocked, e.g. `CreateUserCommand`. */
|
|
12
|
+
readonly requestName: string;
|
|
13
|
+
constructor(flag: string, requestName: string);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=feature-disabled.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-disabled.error.d.ts","sourceRoot":"","sources":["../../src/errors/feature-disabled.error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,YAAY,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAK5C;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link FeatureFlagBehavior} when a handler is gated behind a flag
|
|
4
|
+
* that resolves to disabled and no `fallback` was configured.
|
|
5
|
+
*
|
|
6
|
+
* Over HTTP, register {@link FeatureDisabledFilter}: it answers `403 Forbidden`
|
|
7
|
+
* with the flag, or a plain `404 Not Found` to hide the feature.
|
|
8
|
+
*/
|
|
9
|
+
export class FeatureDisabledError extends Error {
|
|
10
|
+
/** The flag key that gated the request. */
|
|
11
|
+
flag;
|
|
12
|
+
/** The request that was blocked, e.g. `CreateUserCommand`. */
|
|
13
|
+
requestName;
|
|
14
|
+
constructor(flag, requestName) {
|
|
15
|
+
super(`Feature "${flag}" is disabled for ${requestName}`);
|
|
16
|
+
this.name = 'FeatureDisabledError';
|
|
17
|
+
this.flag = flag;
|
|
18
|
+
this.requestName = requestName;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=feature-disabled.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-disabled.error.js","sourceRoot":"","sources":["../../src/errors/feature-disabled.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,2CAA2C;IAClC,IAAI,CAAS;IACtB,8DAA8D;IACrD,WAAW,CAAS;IAE7B,YAAY,IAAY,EAAE,WAAmB;QAC3C,KAAK,CAAC,YAAY,IAAI,qBAAqB,WAAW,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Raised when feature evaluation is configured to surface provider failures. */
|
|
2
|
+
export declare class FeatureFlagEvaluationError extends Error {
|
|
3
|
+
readonly flag: string;
|
|
4
|
+
readonly requestName: string;
|
|
5
|
+
readonly errorCode?: string | undefined;
|
|
6
|
+
readonly providerMessage?: string | undefined;
|
|
7
|
+
readonly name = "FeatureFlagEvaluationError";
|
|
8
|
+
constructor(flag: string, requestName: string, errorCode?: string | undefined, providerMessage?: string | undefined,
|
|
9
|
+
/**
|
|
10
|
+
* The provider error, when one was thrown. It becomes the error's `cause`,
|
|
11
|
+
* which keeps its stack and any provider-specific fields.
|
|
12
|
+
*/
|
|
13
|
+
options?: {
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=feature-flag-evaluation.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flag-evaluation.error.d.ts","sourceRoot":"","sources":["../../src/errors/feature-flag-evaluation.error.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;aAIjC,IAAI,EAAE,MAAM;aACZ,WAAW,EAAE,MAAM;aACnB,SAAS,CAAC,EAAE,MAAM;aAClB,eAAe,CAAC,EAAE,MAAM;IAN1C,SAAkB,IAAI,gCAAgC;IAEtD,YACkB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,eAAe,CAAC,EAAE,MAAM,YAAA;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAQ9B;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
/** Raised when feature evaluation is configured to surface provider failures. */
|
|
3
|
+
export class FeatureFlagEvaluationError extends Error {
|
|
4
|
+
flag;
|
|
5
|
+
requestName;
|
|
6
|
+
errorCode;
|
|
7
|
+
providerMessage;
|
|
8
|
+
name = 'FeatureFlagEvaluationError';
|
|
9
|
+
constructor(flag, requestName, errorCode, providerMessage,
|
|
10
|
+
/**
|
|
11
|
+
* The provider error, when one was thrown. It becomes the error's `cause`,
|
|
12
|
+
* which keeps its stack and any provider-specific fields.
|
|
13
|
+
*/
|
|
14
|
+
options) {
|
|
15
|
+
super(`Feature flag "${flag}" could not be evaluated for ${requestName}` +
|
|
16
|
+
(errorCode ? ` (${errorCode})` : '') +
|
|
17
|
+
(providerMessage ? `: ${providerMessage}` : ''), options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
18
|
+
this.flag = flag;
|
|
19
|
+
this.requestName = requestName;
|
|
20
|
+
this.errorCode = errorCode;
|
|
21
|
+
this.providerMessage = providerMessage;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=feature-flag-evaluation.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flag-evaluation.error.js","sourceRoot":"","sources":["../../src/errors/feature-flag-evaluation.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,iFAAiF;AACjF,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAIjC,IAAI;IACJ,WAAW;IACX,SAAS;IACT,eAAe;IANf,IAAI,GAAG,4BAA4B,CAAC;IAEtD,YACkB,IAAY,EACZ,WAAmB,EACnB,SAAkB,EAClB,eAAwB;IACxC;;;OAGG;IACH,OAA6B;QAE7B,KAAK,CACH,iBAAiB,IAAI,gCAAgC,WAAW,EAAE;YAChE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACpC,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACjD,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CACpE,CAAC;oBAfc,IAAI;2BACJ,WAAW;yBACX,SAAS;+BACT,eAAe;IAajC,CAAC;CACF"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
|
|
2
|
+
import type { Client, EvaluationContext } from '@openfeature/server-sdk';
|
|
3
|
+
import type { FeatureFlagBehaviorOptions, FeatureFlagDecision, TargetingKeyFactory } from './interfaces/feature-flags-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* Unique symbol key set on `context.items` recording the final boolean gate
|
|
6
|
+
* decision (`true` means the handler was allowed to execute).
|
|
7
|
+
*
|
|
8
|
+
* Use this when only the final gate result is needed. For provider variant,
|
|
9
|
+
* reason, error metadata, and targeting identity use
|
|
10
|
+
* {@link FEATURE_FLAG_DECISION_ITEM}.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const isEnabled = context.items.get(FEATURE_FLAG_ITEM) === true;
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const FEATURE_FLAG_ITEM: unique symbol;
|
|
18
|
+
/**
|
|
19
|
+
* Typed token for {@link FEATURE_FLAG_ITEM}: the feature flag gate decision. Reads and writes the same
|
|
20
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const FEATURE_FLAG_ITEM_TOKEN: PipelineItemToken<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Unique symbol key set on `context.items` recording the evaluated feature flag
|
|
25
|
+
* key string.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const flagKey = context.items.get(FEATURE_FLAG_KEY_ITEM) as string | undefined;
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare const FEATURE_FLAG_KEY_ITEM: unique symbol;
|
|
33
|
+
/**
|
|
34
|
+
* Typed token for {@link FEATURE_FLAG_KEY_ITEM}: the evaluated feature flag key. Reads and writes the same
|
|
35
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FEATURE_FLAG_KEY_ITEM_TOKEN: PipelineItemToken<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Unique symbol key set on `context.items` with the full
|
|
40
|
+
* {@link FeatureFlagDecision}: raw boolean value, final enabled decision,
|
|
41
|
+
* provider variant/reason/error metadata, and targeting key when available.
|
|
42
|
+
*
|
|
43
|
+
* This lets audit/logging/telemetry behaviors inspect the exact decision without
|
|
44
|
+
* evaluating the flag a second time.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const decision = context.items.get(FEATURE_FLAG_DECISION_ITEM) as
|
|
49
|
+
* | FeatureFlagDecision
|
|
50
|
+
* | undefined;
|
|
51
|
+
*
|
|
52
|
+
* if (decision?.variant === 'treatment-a') {
|
|
53
|
+
* // enrich telemetry/audit metadata
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare const FEATURE_FLAG_DECISION_ITEM: unique symbol;
|
|
58
|
+
/**
|
|
59
|
+
* Typed token for {@link FEATURE_FLAG_DECISION_ITEM}: the feature flag decision. Reads and writes the same
|
|
60
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const FEATURE_FLAG_DECISION_ITEM_TOKEN: PipelineItemToken<FeatureFlagDecision>;
|
|
63
|
+
/**
|
|
64
|
+
* Pipeline behavior that gates a handler behind an OpenFeature boolean flag.
|
|
65
|
+
*
|
|
66
|
+
* Provider-agnostic by design: it talks only to the OpenFeature {@link Client},
|
|
67
|
+
* so the backing provider (Unleash, Flagsmith, LaunchDarkly, a local file, …) is
|
|
68
|
+
* a drop-in swap of the client passed to the constructor (see `createFeatureFlagClient`).
|
|
69
|
+
* OpenFeature remains the feature-flag abstraction; this behavior adds only the
|
|
70
|
+
* pipeline-specific semantics that an application repeatedly needs.
|
|
71
|
+
*
|
|
72
|
+
* Resolution of the effective options for a handler:
|
|
73
|
+
* 1. Application-wide defaults, the second constructor argument.
|
|
74
|
+
* 2. Per-operation options from the `featureFlag({ ... })` entry,
|
|
75
|
+
* shallow-merged on top of the defaults (handler keys win).
|
|
76
|
+
*
|
|
77
|
+
* Behavior:
|
|
78
|
+
* - No `flag` configured → transparent pass-through.
|
|
79
|
+
* - Flag evaluates `true` and optional `allowedVariants` accepts the provider
|
|
80
|
+
* variant → run the handler.
|
|
81
|
+
* - Final gate disabled → return `fallback(context)` when provided, otherwise
|
|
82
|
+
* throw {@link FeatureDisabledError}.
|
|
83
|
+
* - Provider evaluation error → use the configured `defaultValue` by default,
|
|
84
|
+
* or throw {@link FeatureFlagEvaluationError} with `errorPolicy: 'throw'`.
|
|
85
|
+
*
|
|
86
|
+
* ### Stable rollout identity
|
|
87
|
+
*
|
|
88
|
+
* Percentage/gradual rollouts should use a stable user/account/device/tenant
|
|
89
|
+
* identity through `targetingKeyFactory`.
|
|
90
|
+
*
|
|
91
|
+
* @example Simple gate
|
|
92
|
+
* ```ts
|
|
93
|
+
* class NewCheckoutHandler {
|
|
94
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, { flag: 'new-checkout' }])
|
|
95
|
+
* async handle(command: NewCheckoutCommand) {}
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example Sticky user rollout with graceful fallback
|
|
100
|
+
* ```ts
|
|
101
|
+
* class GetRecommendationsHandler {
|
|
102
|
+
* @pipeline.wrap({ kind: 'query' }, [FeatureFlagBehavior, {
|
|
103
|
+
* flag: 'recommendations-v2',
|
|
104
|
+
* targetingKeyFactory: (ctx) =>
|
|
105
|
+
* ctx.items.get('currentUserId') as string | undefined,
|
|
106
|
+
* fallback: () => [],
|
|
107
|
+
* }])
|
|
108
|
+
* async handle(query: GetRecommendationsQuery) {}
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example Run only for one experiment variant
|
|
113
|
+
* ```ts
|
|
114
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
|
|
115
|
+
* flag: 'checkout-experiment',
|
|
116
|
+
* allowedVariants: ['treatment-a'],
|
|
117
|
+
* }])
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @example Make flag-provider failure visible instead of using the default
|
|
121
|
+
* ```ts
|
|
122
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
|
|
123
|
+
* flag: 'critical-kill-switch',
|
|
124
|
+
* errorPolicy: 'throw',
|
|
125
|
+
* }])
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare class FeatureFlagBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<FeatureFlagBehaviorOptions> {
|
|
129
|
+
private readonly client;
|
|
130
|
+
private readonly moduleContext?;
|
|
131
|
+
private readonly moduleTargetingKeyFactory?;
|
|
132
|
+
static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
|
|
133
|
+
private readonly logger;
|
|
134
|
+
private readonly defaults;
|
|
135
|
+
constructor(client: Client, defaults?: FeatureFlagBehaviorOptions, moduleContext?: EvaluationContext | undefined, logger?: PipelineLogger, moduleTargetingKeyFactory?: TargetingKeyFactory | undefined);
|
|
136
|
+
handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
|
|
137
|
+
/**
|
|
138
|
+
* Resolves OpenFeature details without applying the handler's error policy.
|
|
139
|
+
*
|
|
140
|
+
* Provider failures are normalized into a decision plus a `failure`
|
|
141
|
+
* descriptor so the caller can publish telemetry/audit state before deciding
|
|
142
|
+
* whether to use the default value or throw.
|
|
143
|
+
*/
|
|
144
|
+
private evaluate;
|
|
145
|
+
/** Shallow-merges pipeline-level options over the application defaults. */
|
|
146
|
+
resolveEffectiveOptions(options?: FeatureFlagBehaviorOptions): FeatureFlagBehaviorOptions;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=feature-flag.behavior.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flag.behavior.d.ts","sourceRoot":"","sources":["../src/feature-flag.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAG1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAIzE,OAAO,KAAK,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,iDAAiD,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAAiB,CAAC,OAAO,CACM,CAAC;AAEtE;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,iBAAiB,CAAC,MAAM,CACU,CAAC;AAE7E;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gCAAgC,EAAE,iBAAiB,CAAC,mBAAmB,CAIjF,CAAC;AAWJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,qBAAa,mBACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,0BAA0B,CAAC;IAiD5D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IAnD7C,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAyCrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IAEtD,YACmB,MAAM,EAAE,MAAM,EAC/B,QAAQ,CAAC,EAAE,0BAA0B,EACpB,aAAa,CAAC,EAAE,iBAAiB,YAAA,EAClD,MAAM,CAAC,EAAE,cAAc,EACN,yBAAyB,CAAC,EAAE,mBAAmB,YAAA,EASjE;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CA2ElB;IAED;;;;;;OAMG;YACW,QAAQ;IA2CtB,2EAA2E;IAC3E,uBAAuB,CACrB,OAAO,CAAC,EAAE,0BAA0B,GACnC,0BAA0B,CAE5B;CACF"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
|
|
3
|
+
import { FeatureDisabledError } from './errors/feature-disabled.error.js';
|
|
4
|
+
import { FeatureFlagEvaluationError } from './errors/feature-flag-evaluation.error.js';
|
|
5
|
+
import { buildEvaluationContext } from './helpers/evaluation-context.js';
|
|
6
|
+
/**
|
|
7
|
+
* Unique symbol key set on `context.items` recording the final boolean gate
|
|
8
|
+
* decision (`true` means the handler was allowed to execute).
|
|
9
|
+
*
|
|
10
|
+
* Use this when only the final gate result is needed. For provider variant,
|
|
11
|
+
* reason, error metadata, and targeting identity use
|
|
12
|
+
* {@link FEATURE_FLAG_DECISION_ITEM}.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const isEnabled = context.items.get(FEATURE_FLAG_ITEM) === true;
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const FEATURE_FLAG_ITEM = Symbol('FEATURE_FLAG_ITEM');
|
|
20
|
+
/**
|
|
21
|
+
* Typed token for {@link FEATURE_FLAG_ITEM}: the feature flag gate decision. Reads and writes the same
|
|
22
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
23
|
+
*/
|
|
24
|
+
export const FEATURE_FLAG_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_ITEM', FEATURE_FLAG_ITEM);
|
|
25
|
+
/**
|
|
26
|
+
* Unique symbol key set on `context.items` recording the evaluated feature flag
|
|
27
|
+
* key string.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const flagKey = context.items.get(FEATURE_FLAG_KEY_ITEM) as string | undefined;
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const FEATURE_FLAG_KEY_ITEM = Symbol('FEATURE_FLAG_KEY_ITEM');
|
|
35
|
+
/**
|
|
36
|
+
* Typed token for {@link FEATURE_FLAG_KEY_ITEM}: the evaluated feature flag key. Reads and writes the same
|
|
37
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
38
|
+
*/
|
|
39
|
+
export const FEATURE_FLAG_KEY_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_KEY_ITEM', FEATURE_FLAG_KEY_ITEM);
|
|
40
|
+
/**
|
|
41
|
+
* Unique symbol key set on `context.items` with the full
|
|
42
|
+
* {@link FeatureFlagDecision}: raw boolean value, final enabled decision,
|
|
43
|
+
* provider variant/reason/error metadata, and targeting key when available.
|
|
44
|
+
*
|
|
45
|
+
* This lets audit/logging/telemetry behaviors inspect the exact decision without
|
|
46
|
+
* evaluating the flag a second time.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const decision = context.items.get(FEATURE_FLAG_DECISION_ITEM) as
|
|
51
|
+
* | FeatureFlagDecision
|
|
52
|
+
* | undefined;
|
|
53
|
+
*
|
|
54
|
+
* if (decision?.variant === 'treatment-a') {
|
|
55
|
+
* // enrich telemetry/audit metadata
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export const FEATURE_FLAG_DECISION_ITEM = Symbol('FEATURE_FLAG_DECISION_ITEM');
|
|
60
|
+
/**
|
|
61
|
+
* Typed token for {@link FEATURE_FLAG_DECISION_ITEM}: the feature flag decision. Reads and writes the same
|
|
62
|
+
* `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
|
|
63
|
+
*/
|
|
64
|
+
export const FEATURE_FLAG_DECISION_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_DECISION_ITEM', FEATURE_FLAG_DECISION_ITEM);
|
|
65
|
+
/**
|
|
66
|
+
* Pipeline behavior that gates a handler behind an OpenFeature boolean flag.
|
|
67
|
+
*
|
|
68
|
+
* Provider-agnostic by design: it talks only to the OpenFeature {@link Client},
|
|
69
|
+
* so the backing provider (Unleash, Flagsmith, LaunchDarkly, a local file, …) is
|
|
70
|
+
* a drop-in swap of the client passed to the constructor (see `createFeatureFlagClient`).
|
|
71
|
+
* OpenFeature remains the feature-flag abstraction; this behavior adds only the
|
|
72
|
+
* pipeline-specific semantics that an application repeatedly needs.
|
|
73
|
+
*
|
|
74
|
+
* Resolution of the effective options for a handler:
|
|
75
|
+
* 1. Application-wide defaults, the second constructor argument.
|
|
76
|
+
* 2. Per-operation options from the `featureFlag({ ... })` entry,
|
|
77
|
+
* shallow-merged on top of the defaults (handler keys win).
|
|
78
|
+
*
|
|
79
|
+
* Behavior:
|
|
80
|
+
* - No `flag` configured → transparent pass-through.
|
|
81
|
+
* - Flag evaluates `true` and optional `allowedVariants` accepts the provider
|
|
82
|
+
* variant → run the handler.
|
|
83
|
+
* - Final gate disabled → return `fallback(context)` when provided, otherwise
|
|
84
|
+
* throw {@link FeatureDisabledError}.
|
|
85
|
+
* - Provider evaluation error → use the configured `defaultValue` by default,
|
|
86
|
+
* or throw {@link FeatureFlagEvaluationError} with `errorPolicy: 'throw'`.
|
|
87
|
+
*
|
|
88
|
+
* ### Stable rollout identity
|
|
89
|
+
*
|
|
90
|
+
* Percentage/gradual rollouts should use a stable user/account/device/tenant
|
|
91
|
+
* identity through `targetingKeyFactory`.
|
|
92
|
+
*
|
|
93
|
+
* @example Simple gate
|
|
94
|
+
* ```ts
|
|
95
|
+
* class NewCheckoutHandler {
|
|
96
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, { flag: 'new-checkout' }])
|
|
97
|
+
* async handle(command: NewCheckoutCommand) {}
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example Sticky user rollout with graceful fallback
|
|
102
|
+
* ```ts
|
|
103
|
+
* class GetRecommendationsHandler {
|
|
104
|
+
* @pipeline.wrap({ kind: 'query' }, [FeatureFlagBehavior, {
|
|
105
|
+
* flag: 'recommendations-v2',
|
|
106
|
+
* targetingKeyFactory: (ctx) =>
|
|
107
|
+
* ctx.items.get('currentUserId') as string | undefined,
|
|
108
|
+
* fallback: () => [],
|
|
109
|
+
* }])
|
|
110
|
+
* async handle(query: GetRecommendationsQuery) {}
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @example Run only for one experiment variant
|
|
115
|
+
* ```ts
|
|
116
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
|
|
117
|
+
* flag: 'checkout-experiment',
|
|
118
|
+
* allowedVariants: ['treatment-a'],
|
|
119
|
+
* }])
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Make flag-provider failure visible instead of using the default
|
|
123
|
+
* ```ts
|
|
124
|
+
* @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
|
|
125
|
+
* flag: 'critical-kill-switch',
|
|
126
|
+
* errorPolicy: 'throw',
|
|
127
|
+
* }])
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export class FeatureFlagBehavior {
|
|
131
|
+
client;
|
|
132
|
+
moduleContext;
|
|
133
|
+
moduleTargetingKeyFactory;
|
|
134
|
+
static [PIPELINE_BEHAVIOR_CONTRACT] = {
|
|
135
|
+
validate: (context) => {
|
|
136
|
+
const options = context.effectiveOptions;
|
|
137
|
+
if (options?.flag !== undefined &&
|
|
138
|
+
(typeof options.flag !== 'string' || options.flag.trim() === '')) {
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
handlerName: context.handlerName,
|
|
142
|
+
behaviorName: FeatureFlagBehavior.name,
|
|
143
|
+
message: 'FeatureFlagBehavior `flag` option must be a non-empty string',
|
|
144
|
+
fix: 'Provide a valid string flag name in options or omit options for pass-through.',
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
if ((context.declarationSource === 'handler' ||
|
|
149
|
+
context.declarationSource === 'both') &&
|
|
150
|
+
options?.flag === undefined) {
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
handlerName: context.handlerName,
|
|
154
|
+
behaviorName: FeatureFlagBehavior.name,
|
|
155
|
+
message: 'Explicit FeatureFlagBehavior intent requires a non-empty `flag` name',
|
|
156
|
+
fix: 'Provide flag in the featureFlag({ flag: "flag-name" }) entry.',
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
logger;
|
|
164
|
+
defaults;
|
|
165
|
+
constructor(client, defaults, moduleContext, logger, moduleTargetingKeyFactory) {
|
|
166
|
+
this.client = client;
|
|
167
|
+
this.moduleContext = moduleContext;
|
|
168
|
+
this.moduleTargetingKeyFactory = moduleTargetingKeyFactory;
|
|
169
|
+
if (typeof client?.getBooleanDetails !== 'function') {
|
|
170
|
+
throw new TypeError('FeatureFlagBehavior requires an OpenFeature client, such as the one createFeatureFlagClient() resolves.');
|
|
171
|
+
}
|
|
172
|
+
this.defaults = defaults ?? {};
|
|
173
|
+
this.logger = logger ?? console;
|
|
174
|
+
}
|
|
175
|
+
async handle(context, next) {
|
|
176
|
+
const options = this.resolveEffectiveOptions(context.getBehaviorOptions(FeatureFlagBehavior));
|
|
177
|
+
if (!options.flag)
|
|
178
|
+
return next();
|
|
179
|
+
const targetingKeyFactory = options.targetingKeyFactory ?? this.moduleTargetingKeyFactory;
|
|
180
|
+
const evaluationContext = buildEvaluationContext(context, this.moduleContext, options.context, targetingKeyFactory);
|
|
181
|
+
const defaultValue = options.defaultValue ?? false;
|
|
182
|
+
const { details, failure } = await this.evaluate(options.flag, defaultValue, evaluationContext);
|
|
183
|
+
// A boolean true is necessary but, when allowedVariants is configured, not
|
|
184
|
+
// sufficient: the provider must also resolve one of the selected variants.
|
|
185
|
+
const variantAllowed = !options.allowedVariants?.length ||
|
|
186
|
+
(!!details.variant && options.allowedVariants.includes(details.variant));
|
|
187
|
+
const enabled = details.value && variantAllowed;
|
|
188
|
+
const decision = {
|
|
189
|
+
flagKey: options.flag,
|
|
190
|
+
value: details.value,
|
|
191
|
+
enabled,
|
|
192
|
+
...(details.variant ? { variant: details.variant } : {}),
|
|
193
|
+
...(details.reason ? { reason: details.reason } : {}),
|
|
194
|
+
...(details.errorCode !== undefined
|
|
195
|
+
? { errorCode: String(details.errorCode) }
|
|
196
|
+
: {}),
|
|
197
|
+
...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),
|
|
198
|
+
...(evaluationContext.targetingKey
|
|
199
|
+
? { targetingKey: evaluationContext.targetingKey }
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
// Publish the decision before applying the throw policy so outer audit and
|
|
203
|
+
// telemetry behaviors can observe provider failures.
|
|
204
|
+
setPipelineItem(context, FEATURE_FLAG_KEY_ITEM_TOKEN, options.flag);
|
|
205
|
+
setPipelineItem(context, FEATURE_FLAG_ITEM_TOKEN, enabled);
|
|
206
|
+
setPipelineItem(context, FEATURE_FLAG_DECISION_ITEM_TOKEN, decision);
|
|
207
|
+
if (failure && (options.errorPolicy ?? 'use-default') === 'throw') {
|
|
208
|
+
throw new FeatureFlagEvaluationError(options.flag, context.requestName, failure.errorCode, failure.message, { cause: failure.cause });
|
|
209
|
+
}
|
|
210
|
+
const variantSuffix = details.variant ? ` variant=${details.variant}` : '';
|
|
211
|
+
const reasonSuffix = details.reason ? ` reason=${details.reason}` : '';
|
|
212
|
+
const state = enabled ? 'enabled' : 'disabled';
|
|
213
|
+
this.logger.debug?.(`Feature "${options.flag}" ${state} for ${context.requestName}${variantSuffix}${reasonSuffix}`, FeatureFlagBehavior.name);
|
|
214
|
+
if (enabled)
|
|
215
|
+
return next();
|
|
216
|
+
if (options.fallback)
|
|
217
|
+
return options.fallback(context);
|
|
218
|
+
throw new FeatureDisabledError(options.flag, context.requestName);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolves OpenFeature details without applying the handler's error policy.
|
|
222
|
+
*
|
|
223
|
+
* Provider failures are normalized into a decision plus a `failure`
|
|
224
|
+
* descriptor so the caller can publish telemetry/audit state before deciding
|
|
225
|
+
* whether to use the default value or throw.
|
|
226
|
+
*/
|
|
227
|
+
async evaluate(flag, defaultValue, evaluationContext) {
|
|
228
|
+
let details;
|
|
229
|
+
try {
|
|
230
|
+
details = (await this.client.getBooleanDetails(flag, defaultValue, evaluationContext));
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return {
|
|
235
|
+
details: {
|
|
236
|
+
value: defaultValue,
|
|
237
|
+
reason: 'ERROR',
|
|
238
|
+
errorMessage: message,
|
|
239
|
+
},
|
|
240
|
+
failure: { message, cause: error },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// A provider that reports an error in its details rather than throwing is
|
|
244
|
+
// the same outcome from the application's point of view.
|
|
245
|
+
if (details.errorCode !== undefined) {
|
|
246
|
+
return {
|
|
247
|
+
details,
|
|
248
|
+
failure: {
|
|
249
|
+
message: details.errorMessage ?? String(details.errorCode),
|
|
250
|
+
errorCode: String(details.errorCode),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { details };
|
|
255
|
+
}
|
|
256
|
+
/** Shallow-merges pipeline-level options over the application defaults. */
|
|
257
|
+
resolveEffectiveOptions(options) {
|
|
258
|
+
return options ? { ...this.defaults, ...options } : this.defaults;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=feature-flag.behavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flag.behavior.js","sourceRoot":"","sources":["../src/feature-flag.behavior.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type IPipelineContext } from '@cqrs-ddd/pipeline';
|
|
2
|
+
/**
|
|
3
|
+
* The feature-flag decision of one execution as flat attributes, named after
|
|
4
|
+
* the OpenTelemetry feature-flag conventions: `feature_flag.key` and
|
|
5
|
+
* `feature_flag.enabled`, plus `feature_flag.variant` and `feature_flag.reason`
|
|
6
|
+
* when the provider reported them, and `feature_flag.error_code` only when
|
|
7
|
+
* evaluation failed. `{}` when `FeatureFlagBehavior` did not run. The targeting
|
|
8
|
+
* key and the provider's error message are never included.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain data and needs no telemetry package: put it on a span, a
|
|
11
|
+
* log line, an audit record or a metric label (bounded values only).
|
|
12
|
+
*
|
|
13
|
+
* @param context - The pipeline execution, read after `FeatureFlagBehavior` ran.
|
|
14
|
+
* @returns The attributes; empty when the behavior did not run.
|
|
15
|
+
*
|
|
16
|
+
* @example Span attributes
|
|
17
|
+
* With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
|
|
18
|
+
* ```ts
|
|
19
|
+
* createPipeline({
|
|
20
|
+
* globalBehaviors: {
|
|
21
|
+
* before: [TraceBehavior, [AttributesBehavior, { factories: [buildFeatureFlagAttributes] }]],
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Audit metadata
|
|
27
|
+
* `audit()` is declared outside `featureFlag()`, so the record is built after its decision:
|
|
28
|
+
* ```ts
|
|
29
|
+
* @pipeline.wrap(
|
|
30
|
+
* { kind: 'command' },
|
|
31
|
+
* audit({ action: 'order.create', metadata: buildFeatureFlagAttributes }),
|
|
32
|
+
* featureFlag({ flag: 'checkout' }),
|
|
33
|
+
* )
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example A log line from a custom behavior, after the chain ran
|
|
37
|
+
* ```ts
|
|
38
|
+
* async handle(context: IPipelineContext, next: NextDelegate) {
|
|
39
|
+
* try {
|
|
40
|
+
* return await next();
|
|
41
|
+
* } finally {
|
|
42
|
+
* this.logger.log({ request: context.requestName, ...buildFeatureFlagAttributes(context) });
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildFeatureFlagAttributes(context: IPipelineContext): Record<string, string | boolean>;
|
|
48
|
+
//# sourceMappingURL=build-attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAYlC"}
|