@mergedapp/feature-flags 0.1.3
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/README.md +651 -0
- package/dist/cjs/cli/audit.js +117 -0
- package/dist/cjs/cli/cleanup.js +105 -0
- package/dist/cjs/cli/config-loader.js +102 -0
- package/dist/cjs/cli/generate.js +194 -0
- package/dist/cjs/cli/parse-args.js +18 -0
- package/dist/cjs/cli.js +46 -0
- package/dist/cjs/client.js +505 -0
- package/dist/cjs/errors.js +24 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/jwt.js +85 -0
- package/dist/cjs/nestjs/bindings.js +36 -0
- package/dist/cjs/nestjs/constants.js +7 -0
- package/dist/cjs/nestjs/context.js +28 -0
- package/dist/cjs/nestjs/decorators.js +50 -0
- package/dist/cjs/nestjs/errors.js +25 -0
- package/dist/cjs/nestjs/evaluator.js +87 -0
- package/dist/cjs/nestjs/guard.js +67 -0
- package/dist/cjs/nestjs/interceptor.js +56 -0
- package/dist/cjs/nestjs/module.js +70 -0
- package/dist/cjs/nestjs/service.js +54 -0
- package/dist/cjs/nestjs/types.js +2 -0
- package/dist/cjs/nestjs.js +26 -0
- package/dist/cjs/openfeature/context.js +166 -0
- package/dist/cjs/openfeature/hooks.js +31 -0
- package/dist/cjs/openfeature/server-provider.js +107 -0
- package/dist/cjs/openfeature/server.js +13 -0
- package/dist/cjs/openfeature/shared.js +83 -0
- package/dist/cjs/openfeature/web-provider.js +156 -0
- package/dist/cjs/openfeature/web.js +13 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/persistence.js +249 -0
- package/dist/cjs/react/hooks.js +86 -0
- package/dist/cjs/react/provider.js +106 -0
- package/dist/cjs/react.js +7 -0
- package/dist/cjs/remote-evaluator.js +162 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cli/audit.d.ts +3 -0
- package/dist/cli/audit.js +114 -0
- package/dist/cli/cleanup.d.ts +3 -0
- package/dist/cli/cleanup.js +102 -0
- package/dist/cli/config-loader.d.ts +26 -0
- package/dist/cli/config-loader.js +66 -0
- package/dist/cli/generate.d.ts +3 -0
- package/dist/cli/generate.js +191 -0
- package/dist/cli/parse-args.d.ts +1 -0
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +45 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.js +501 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.js +78 -0
- package/dist/nestjs/bindings.d.ts +5 -0
- package/dist/nestjs/bindings.js +33 -0
- package/dist/nestjs/constants.d.ts +4 -0
- package/dist/nestjs/constants.js +4 -0
- package/dist/nestjs/context.d.ts +12 -0
- package/dist/nestjs/context.js +24 -0
- package/dist/nestjs/decorators.d.ts +4 -0
- package/dist/nestjs/decorators.js +45 -0
- package/dist/nestjs/errors.d.ts +12 -0
- package/dist/nestjs/errors.js +20 -0
- package/dist/nestjs/evaluator.d.ts +17 -0
- package/dist/nestjs/evaluator.js +83 -0
- package/dist/nestjs/guard.d.ts +19 -0
- package/dist/nestjs/guard.js +63 -0
- package/dist/nestjs/interceptor.d.ts +10 -0
- package/dist/nestjs/interceptor.js +53 -0
- package/dist/nestjs/module.d.ts +6 -0
- package/dist/nestjs/module.js +67 -0
- package/dist/nestjs/service.d.ts +30 -0
- package/dist/nestjs/service.js +51 -0
- package/dist/nestjs/types.d.ts +100 -0
- package/dist/nestjs/types.js +1 -0
- package/dist/nestjs.cjs +1 -0
- package/dist/nestjs.d.ts +10 -0
- package/dist/nestjs.js +9 -0
- package/dist/openfeature/context.d.ts +10 -0
- package/dist/openfeature/context.js +160 -0
- package/dist/openfeature/hooks.d.ts +6 -0
- package/dist/openfeature/hooks.js +27 -0
- package/dist/openfeature/server-provider.d.ts +20 -0
- package/dist/openfeature/server-provider.js +102 -0
- package/dist/openfeature/server.cjs +1 -0
- package/dist/openfeature/server.d.ts +3 -0
- package/dist/openfeature/server.js +3 -0
- package/dist/openfeature/shared.d.ts +37 -0
- package/dist/openfeature/shared.js +74 -0
- package/dist/openfeature/web-provider.d.ts +27 -0
- package/dist/openfeature/web-provider.js +151 -0
- package/dist/openfeature/web.cjs +1 -0
- package/dist/openfeature/web.d.ts +3 -0
- package/dist/openfeature/web.js +3 -0
- package/dist/persistence.d.ts +39 -0
- package/dist/persistence.js +203 -0
- package/dist/react/hooks.d.ts +52 -0
- package/dist/react/hooks.js +78 -0
- package/dist/react/provider.d.ts +71 -0
- package/dist/react/provider.js +99 -0
- package/dist/react.cjs +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/remote-evaluator.d.ts +28 -0
- package/dist/remote-evaluator.js +158 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/featureflags.config.schema.json +38 -0
- package/package.json +107 -0
package/dist/nestjs.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createTypedNestjsBindings, } from "./nestjs/bindings.js";
|
|
2
|
+
export { FeatureFlagContext, FeatureFlagGate, RequireFeatureFlag, } from "./nestjs/decorators.js";
|
|
3
|
+
export { FeatureFlagsRequestContextStore, resolveFeatureFlagContext } from "./nestjs/context.js";
|
|
4
|
+
export { FeatureFlagDisabledHttpException, FeatureFlagGateError } from "./nestjs/errors.js";
|
|
5
|
+
export { NativeNestFeatureFlagsEvaluator, type FeatureFlagsEvaluationRequest, type NestFeatureFlagsEvaluator, } from "./nestjs/evaluator.js";
|
|
6
|
+
export { FeatureFlagGuard, evaluateGate, type RequireFeatureFlagMetadata } from "./nestjs/guard.js";
|
|
7
|
+
export { FeatureFlagContextInterceptor } from "./nestjs/interceptor.js";
|
|
8
|
+
export { FeatureFlagsModule, } from "./nestjs/module.js";
|
|
9
|
+
export { FeatureFlagsService } from "./nestjs/service.js";
|
|
10
|
+
export type { BooleanFlagKey, ComparableFlagKey, FeatureFlagContextFactory, FeatureFlagEvaluationDetails, FeatureFlagEvaluationMetadata, FeatureFlagEvaluationReason, FlagKey, NativeFeatureFlagsModuleAsyncOptions, NativeFeatureFlagsModuleOptions, NonBooleanComparableFlagKey, ObjectFlagKey, RawFeatureFlagGateOptions, RawRequireFeatureFlagOptions, ScalarFlagKey, StringFlagKey, NumberFlagKey, TypedFeatureFlagGateOptions, TypedFeatureFlagsModuleAsyncOptions, TypedFeatureFlagsModuleFacade, TypedFeatureFlagsModuleOptions, TypedNestjsBindings, TypedRequireFeatureFlagOptions, } from "./nestjs/types.js";
|
package/dist/nestjs.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createTypedNestjsBindings, } from "./nestjs/bindings.js";
|
|
2
|
+
export { FeatureFlagContext, FeatureFlagGate, RequireFeatureFlag, } from "./nestjs/decorators.js";
|
|
3
|
+
export { FeatureFlagsRequestContextStore, resolveFeatureFlagContext } from "./nestjs/context.js";
|
|
4
|
+
export { FeatureFlagDisabledHttpException, FeatureFlagGateError } from "./nestjs/errors.js";
|
|
5
|
+
export { NativeNestFeatureFlagsEvaluator, } from "./nestjs/evaluator.js";
|
|
6
|
+
export { FeatureFlagGuard, evaluateGate } from "./nestjs/guard.js";
|
|
7
|
+
export { FeatureFlagContextInterceptor } from "./nestjs/interceptor.js";
|
|
8
|
+
export { FeatureFlagsModule, } from "./nestjs/module.js";
|
|
9
|
+
export { FeatureFlagsService } from "./nestjs/service.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EvaluationContext, EvaluationContextValue } from "@openfeature/core";
|
|
2
|
+
import type { FeatureFlagEvaluationContext, FeatureFlagEvaluationContextValue } from "@repo/types-v2";
|
|
3
|
+
export type OpenFeatureContextMapper = (context: EvaluationContext) => FeatureFlagEvaluationContext | null;
|
|
4
|
+
export type MergeableFeatureFlagContext = FeatureFlagEvaluationContext | null | undefined;
|
|
5
|
+
export declare const DEFAULT_TARGETING_KEY_ATTRIBUTE = "subject.key";
|
|
6
|
+
export declare function mapOpenFeatureContextValue(value: EvaluationContextValue, visited?: WeakSet<object>): FeatureFlagEvaluationContextValue;
|
|
7
|
+
export declare function createDefaultOpenFeatureContextMapper(params?: {
|
|
8
|
+
targetingKeyAttribute?: string;
|
|
9
|
+
}): OpenFeatureContextMapper;
|
|
10
|
+
export declare function mergeFeatureFlagEvaluationContexts(...contexts: MergeableFeatureFlagContext[]): FeatureFlagEvaluationContext | null;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export const DEFAULT_TARGETING_KEY_ATTRIBUTE = "subject.key";
|
|
2
|
+
const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
3
|
+
const CYCLIC_CONTEXT_ERROR_MESSAGE = "Cyclic feature flag evaluation context is not allowed.";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isSafeObjectKey(key) {
|
|
8
|
+
return !UNSAFE_OBJECT_KEYS.has(key);
|
|
9
|
+
}
|
|
10
|
+
function createAttributeRecord() {
|
|
11
|
+
return Object.create(null);
|
|
12
|
+
}
|
|
13
|
+
function beginTraversal(value, visited) {
|
|
14
|
+
if (visited.has(value)) {
|
|
15
|
+
throw new TypeError(CYCLIC_CONTEXT_ERROR_MESSAGE);
|
|
16
|
+
}
|
|
17
|
+
visited.add(value);
|
|
18
|
+
}
|
|
19
|
+
function assertAcyclicFeatureFlagContextValue(value, visited = new WeakSet()) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
beginTraversal(value, visited);
|
|
22
|
+
try {
|
|
23
|
+
for (const entry of value) {
|
|
24
|
+
assertAcyclicFeatureFlagContextValue(entry, visited);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
visited.delete(value);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(value)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
beginTraversal(value, visited);
|
|
36
|
+
try {
|
|
37
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
38
|
+
if (!isSafeObjectKey(key)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
assertAcyclicFeatureFlagContextValue(entry, visited);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
visited.delete(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function setNestedAttribute(target, path, value) {
|
|
49
|
+
const segments = path.split(".").filter(Boolean);
|
|
50
|
+
const head = segments[0];
|
|
51
|
+
if (!head || !isSafeObjectKey(head)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const tail = segments.slice(1);
|
|
55
|
+
if (tail.length === 0) {
|
|
56
|
+
target[head] = value;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const nextTarget = isRecord(target[head]) ? target[head] : createAttributeRecord();
|
|
60
|
+
target[head] = nextTarget;
|
|
61
|
+
setNestedAttribute(nextTarget, tail.join("."), value);
|
|
62
|
+
}
|
|
63
|
+
function mergeAttributeValue(left, right, visited = new WeakSet()) {
|
|
64
|
+
if (isRecord(left) && isRecord(right)) {
|
|
65
|
+
beginTraversal(left, visited);
|
|
66
|
+
if (right !== left) {
|
|
67
|
+
beginTraversal(right, visited);
|
|
68
|
+
}
|
|
69
|
+
const merged = createAttributeRecord();
|
|
70
|
+
try {
|
|
71
|
+
for (const [key, value] of Object.entries(left)) {
|
|
72
|
+
if (!isSafeObjectKey(key)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
merged[key] = value;
|
|
76
|
+
}
|
|
77
|
+
for (const [key, value] of Object.entries(right)) {
|
|
78
|
+
if (!isSafeObjectKey(key)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
merged[key] = typeof merged[key] === "undefined" ? value : mergeAttributeValue(merged[key], value, visited);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
visited.delete(left);
|
|
86
|
+
if (right !== left) {
|
|
87
|
+
visited.delete(right);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return merged;
|
|
91
|
+
}
|
|
92
|
+
return right;
|
|
93
|
+
}
|
|
94
|
+
export function mapOpenFeatureContextValue(value, visited = new WeakSet()) {
|
|
95
|
+
if (value instanceof Date) {
|
|
96
|
+
return value.toISOString();
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
beginTraversal(value, visited);
|
|
100
|
+
try {
|
|
101
|
+
return value.map((entry) => mapOpenFeatureContextValue(entry, visited));
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
visited.delete(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (value && typeof value === "object") {
|
|
108
|
+
beginTraversal(value, visited);
|
|
109
|
+
const mapped = createAttributeRecord();
|
|
110
|
+
try {
|
|
111
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
112
|
+
if (!isSafeObjectKey(key)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
mapped[key] = mapOpenFeatureContextValue(entry, visited);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
visited.delete(value);
|
|
120
|
+
}
|
|
121
|
+
return mapped;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
export function createDefaultOpenFeatureContextMapper(params) {
|
|
126
|
+
const targetingKeyAttribute = params?.targetingKeyAttribute ?? DEFAULT_TARGETING_KEY_ATTRIBUTE;
|
|
127
|
+
return (context) => {
|
|
128
|
+
const attributes = createAttributeRecord();
|
|
129
|
+
if (context.targetingKey) {
|
|
130
|
+
setNestedAttribute(attributes, targetingKeyAttribute, context.targetingKey);
|
|
131
|
+
}
|
|
132
|
+
for (const [key, value] of Object.entries(context)) {
|
|
133
|
+
if (key === "targetingKey" || typeof value === "undefined") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!isSafeObjectKey(key)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
attributes[key] = mapOpenFeatureContextValue(value);
|
|
140
|
+
}
|
|
141
|
+
return Object.keys(attributes).length > 0 ? { attributes } : null;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function mergeFeatureFlagEvaluationContexts(...contexts) {
|
|
145
|
+
const mergedAttributes = createAttributeRecord();
|
|
146
|
+
for (const context of contexts) {
|
|
147
|
+
if (!context?.attributes) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const [key, value] of Object.entries(context.attributes)) {
|
|
151
|
+
if (!isSafeObjectKey(key)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
assertAcyclicFeatureFlagContextValue(value);
|
|
155
|
+
mergedAttributes[key] =
|
|
156
|
+
typeof mergedAttributes[key] === "undefined" ? value : mergeAttributeValue(mergedAttributes[key], value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return Object.keys(mergedAttributes).length > 0 ? { attributes: mergedAttributes } : null;
|
|
160
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BaseHook, EvaluationContext } from "@openfeature/core";
|
|
2
|
+
export declare function createStaticContextHook(context: EvaluationContext): BaseHook;
|
|
3
|
+
export declare function createLoggingHook(params: {
|
|
4
|
+
logger: Pick<Console, "debug" | "error">;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
}): BaseHook;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function createStaticContextHook(context) {
|
|
2
|
+
return {
|
|
3
|
+
before() {
|
|
4
|
+
return context;
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function createLoggingHook(params) {
|
|
9
|
+
const prefix = params.prefix ?? "feature-flags";
|
|
10
|
+
return {
|
|
11
|
+
after(hookContext, evaluationDetails, hookHints) {
|
|
12
|
+
params.logger.debug(`[${prefix}] evaluated flag (${hookContext.flagKey})`, {
|
|
13
|
+
defaultValue: hookContext.defaultValue,
|
|
14
|
+
errorCode: evaluationDetails.errorCode,
|
|
15
|
+
hookHints,
|
|
16
|
+
reason: evaluationDetails.reason,
|
|
17
|
+
value: evaluationDetails.value,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
error(hookContext, error, hookHints) {
|
|
21
|
+
params.logger.error(`[${prefix}] failed evaluating flag (${hookContext.flagKey})`, {
|
|
22
|
+
error,
|
|
23
|
+
hookHints,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OpenFeatureEventEmitter, type EvaluationContext, type JsonValue, type Logger, type Provider, type ProviderMetadata, type ResolutionDetails } from "@openfeature/server-sdk";
|
|
2
|
+
import { type OpenFeatureAdapterConfig } from "./shared.js";
|
|
3
|
+
export type OpenFeatureServerProviderConfig = OpenFeatureAdapterConfig;
|
|
4
|
+
export declare class MergedOpenFeatureServerProvider implements Provider {
|
|
5
|
+
readonly events: OpenFeatureEventEmitter;
|
|
6
|
+
readonly metadata: ProviderMetadata;
|
|
7
|
+
readonly runsOn: "server";
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly evaluator;
|
|
10
|
+
private readonly evaluationCache;
|
|
11
|
+
constructor(config: OpenFeatureServerProviderConfig);
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
onClose(): Promise<void>;
|
|
14
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<boolean>>;
|
|
15
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<string>>;
|
|
16
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<number>>;
|
|
17
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, logger: Logger): Promise<ResolutionDetails<T>>;
|
|
18
|
+
private resolveEvaluation;
|
|
19
|
+
}
|
|
20
|
+
export declare function createOpenFeatureServerProvider(config: OpenFeatureServerProviderConfig): MergedOpenFeatureServerProvider;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { OpenFeatureEventEmitter, } from "@openfeature/server-sdk";
|
|
2
|
+
import { RemoteFeatureFlagEvaluator } from "../remote-evaluator.js";
|
|
3
|
+
import { findEvaluatedFlag, matchesExpectedType, resolveMissingFlagEvaluation, resolveOpenFeatureContext, resolveSuccessfulEvaluation, resolveTypeMismatchEvaluation, } from "./shared.js";
|
|
4
|
+
export class MergedOpenFeatureServerProvider {
|
|
5
|
+
events = new OpenFeatureEventEmitter();
|
|
6
|
+
metadata;
|
|
7
|
+
runsOn = "server";
|
|
8
|
+
config;
|
|
9
|
+
evaluator;
|
|
10
|
+
evaluationCache = new Map();
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.evaluator = new RemoteFeatureFlagEvaluator(config);
|
|
14
|
+
this.metadata = {
|
|
15
|
+
name: config.metadataName ?? "@mergedapp/feature-flags/openfeature-server",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async initialize() {
|
|
19
|
+
this.events.emit("PROVIDER_READY");
|
|
20
|
+
}
|
|
21
|
+
async onClose() {
|
|
22
|
+
this.evaluationCache.clear();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
async resolveBooleanEvaluation(flagKey, defaultValue, context, logger) {
|
|
26
|
+
return this.resolveEvaluation({
|
|
27
|
+
context,
|
|
28
|
+
defaultValue,
|
|
29
|
+
expectedType: "BOOLEAN",
|
|
30
|
+
flagKey,
|
|
31
|
+
logger,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async resolveStringEvaluation(flagKey, defaultValue, context, logger) {
|
|
35
|
+
return this.resolveEvaluation({
|
|
36
|
+
context,
|
|
37
|
+
defaultValue,
|
|
38
|
+
expectedType: "STRING",
|
|
39
|
+
flagKey,
|
|
40
|
+
logger,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async resolveNumberEvaluation(flagKey, defaultValue, context, logger) {
|
|
44
|
+
return this.resolveEvaluation({
|
|
45
|
+
context,
|
|
46
|
+
defaultValue,
|
|
47
|
+
expectedType: "NUMBER",
|
|
48
|
+
flagKey,
|
|
49
|
+
logger,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async resolveObjectEvaluation(flagKey, defaultValue, context, logger) {
|
|
53
|
+
return this.resolveEvaluation({
|
|
54
|
+
context,
|
|
55
|
+
defaultValue,
|
|
56
|
+
expectedType: "OBJECT",
|
|
57
|
+
flagKey,
|
|
58
|
+
logger,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async resolveEvaluation(params) {
|
|
62
|
+
try {
|
|
63
|
+
const mappedContext = resolveOpenFeatureContext({ config: this.config, context: params.context });
|
|
64
|
+
const cacheKey = JSON.stringify(mappedContext ?? null);
|
|
65
|
+
let evaluationPromise = this.evaluationCache.get(cacheKey);
|
|
66
|
+
if (!evaluationPromise) {
|
|
67
|
+
evaluationPromise = this.evaluator.evaluate(mappedContext).finally(() => {
|
|
68
|
+
this.evaluationCache.delete(cacheKey);
|
|
69
|
+
});
|
|
70
|
+
this.evaluationCache.set(cacheKey, evaluationPromise);
|
|
71
|
+
}
|
|
72
|
+
const evaluation = await evaluationPromise;
|
|
73
|
+
const flag = findEvaluatedFlag(evaluation.flags, params.flagKey, this.config.flagIds);
|
|
74
|
+
if (!flag) {
|
|
75
|
+
return resolveMissingFlagEvaluation({
|
|
76
|
+
defaultValue: params.defaultValue,
|
|
77
|
+
flagKey: params.flagKey,
|
|
78
|
+
logger: params.logger,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!matchesExpectedType(flag, params.expectedType)) {
|
|
82
|
+
return resolveTypeMismatchEvaluation({
|
|
83
|
+
actualFlag: flag,
|
|
84
|
+
defaultValue: params.defaultValue,
|
|
85
|
+
expectedType: params.expectedType,
|
|
86
|
+
logger: params.logger,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return resolveSuccessfulEvaluation({
|
|
90
|
+
defaultValue: params.defaultValue,
|
|
91
|
+
flag,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
params.logger.error(`Failed evaluating feature flag (${params.flagKey}).`, error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function createOpenFeatureServerProvider(config) {
|
|
101
|
+
return new MergedOpenFeatureServerProvider(config);
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./cjs/openfeature/server.js")
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createLoggingHook, createStaticContextHook } from "./hooks.js";
|
|
2
|
+
export { DEFAULT_TARGETING_KEY_ATTRIBUTE, createDefaultOpenFeatureContextMapper, mergeFeatureFlagEvaluationContexts, type OpenFeatureContextMapper, } from "./context.js";
|
|
3
|
+
export { createOpenFeatureServerProvider, MergedOpenFeatureServerProvider, type OpenFeatureServerProviderConfig, } from "./server-provider.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createLoggingHook, createStaticContextHook } from "./hooks.js";
|
|
2
|
+
export { DEFAULT_TARGETING_KEY_ATTRIBUTE, createDefaultOpenFeatureContextMapper, mergeFeatureFlagEvaluationContexts, } from "./context.js";
|
|
3
|
+
export { createOpenFeatureServerProvider, MergedOpenFeatureServerProvider, } from "./server-provider.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { EvaluationContext, Logger } from "@openfeature/core";
|
|
2
|
+
import { type FlagMetadata, type JsonValue, type ResolutionDetails } from "@openfeature/core";
|
|
3
|
+
import type { EvaluatedFlag, FeatureFlagEvaluationContext } from "@repo/types-v2";
|
|
4
|
+
import type { FeatureFlagClientConfig, FeatureFlagRuntimeStatus } from "../types.js";
|
|
5
|
+
import { type OpenFeatureContextMapper } from "./context.js";
|
|
6
|
+
export type OpenFeatureAdapterConfig = Pick<FeatureFlagClientConfig, "apiUrl" | "clientKey" | "environmentId" | "evaluationContext" | "flagIds" | "organizationId" | "publicKey" | "refreshInterval" | "teamId"> & {
|
|
7
|
+
contextMapper?: OpenFeatureContextMapper;
|
|
8
|
+
metadataName?: string;
|
|
9
|
+
targetingKeyAttribute?: string;
|
|
10
|
+
};
|
|
11
|
+
export type FeatureFlagPrimitive = boolean | number | string | JsonValue;
|
|
12
|
+
export declare function resolveOpenFeatureContext(params: {
|
|
13
|
+
config: OpenFeatureAdapterConfig;
|
|
14
|
+
context: EvaluationContext | null | undefined;
|
|
15
|
+
}): FeatureFlagEvaluationContext | null;
|
|
16
|
+
export declare function createOpenFeatureMetadata(params: {
|
|
17
|
+
flag: EvaluatedFlag;
|
|
18
|
+
runtimeStatus?: FeatureFlagRuntimeStatus | null;
|
|
19
|
+
}): FlagMetadata;
|
|
20
|
+
export declare function findEvaluatedFlag(flags: EvaluatedFlag[], flagKey: string, flagIds?: Record<string, string>): EvaluatedFlag | undefined;
|
|
21
|
+
export declare function resolveSuccessfulEvaluation<T extends FeatureFlagPrimitive>(params: {
|
|
22
|
+
defaultValue: T;
|
|
23
|
+
flag: EvaluatedFlag;
|
|
24
|
+
runtimeStatus?: FeatureFlagRuntimeStatus | null;
|
|
25
|
+
}): ResolutionDetails<T>;
|
|
26
|
+
export declare function resolveMissingFlagEvaluation<T extends FeatureFlagPrimitive>(params: {
|
|
27
|
+
defaultValue: T;
|
|
28
|
+
flagKey: string;
|
|
29
|
+
logger?: Logger;
|
|
30
|
+
}): ResolutionDetails<T>;
|
|
31
|
+
export declare function resolveTypeMismatchEvaluation<T extends FeatureFlagPrimitive>(params: {
|
|
32
|
+
actualFlag: EvaluatedFlag;
|
|
33
|
+
defaultValue: T;
|
|
34
|
+
expectedType: "BOOLEAN" | "NUMBER" | "OBJECT" | "STRING";
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
}): ResolutionDetails<T>;
|
|
37
|
+
export declare function matchesExpectedType(flag: EvaluatedFlag, expectedType: "BOOLEAN" | "NUMBER" | "OBJECT" | "STRING"): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ErrorCode, StandardResolutionReasons, } from "@openfeature/core";
|
|
2
|
+
import { createDefaultOpenFeatureContextMapper, mergeFeatureFlagEvaluationContexts, } from "./context.js";
|
|
3
|
+
export function resolveOpenFeatureContext(params) {
|
|
4
|
+
const mapper = params.config.contextMapper ??
|
|
5
|
+
createDefaultOpenFeatureContextMapper({
|
|
6
|
+
targetingKeyAttribute: params.config.targetingKeyAttribute,
|
|
7
|
+
});
|
|
8
|
+
return mergeFeatureFlagEvaluationContexts(params.config.evaluationContext ?? null, params.context ? mapper(params.context) : null);
|
|
9
|
+
}
|
|
10
|
+
export function createOpenFeatureMetadata(params) {
|
|
11
|
+
return {
|
|
12
|
+
id: params.flag.id,
|
|
13
|
+
name: params.flag.name,
|
|
14
|
+
type: params.flag.type,
|
|
15
|
+
teamId: params.flag.teamId ?? "",
|
|
16
|
+
enabled: params.flag.enabled,
|
|
17
|
+
source: params.runtimeStatus?.source ?? "network",
|
|
18
|
+
isStale: params.runtimeStatus?.isStale ?? false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function findEvaluatedFlag(flags, flagKey, flagIds) {
|
|
22
|
+
const resolvedId = flagIds?.[flagKey];
|
|
23
|
+
if (resolvedId) {
|
|
24
|
+
const matchedByResolvedId = flags.find((flag) => flag.id === resolvedId);
|
|
25
|
+
if (matchedByResolvedId) {
|
|
26
|
+
return matchedByResolvedId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return flags.find((flag) => flag.id === flagKey) ?? flags.find((flag) => flag.name === flagKey);
|
|
30
|
+
}
|
|
31
|
+
export function resolveSuccessfulEvaluation(params) {
|
|
32
|
+
return {
|
|
33
|
+
value: params.flag.value,
|
|
34
|
+
variant: params.flag.name,
|
|
35
|
+
flagMetadata: createOpenFeatureMetadata(params),
|
|
36
|
+
reason: resolveResolutionReason(params),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveResolutionReason(params) {
|
|
40
|
+
if (!params.flag.enabled) {
|
|
41
|
+
return StandardResolutionReasons.DISABLED;
|
|
42
|
+
}
|
|
43
|
+
if (params.runtimeStatus?.isStale) {
|
|
44
|
+
return StandardResolutionReasons.STALE;
|
|
45
|
+
}
|
|
46
|
+
if (params.runtimeStatus?.source === "persisted") {
|
|
47
|
+
return StandardResolutionReasons.CACHED;
|
|
48
|
+
}
|
|
49
|
+
return StandardResolutionReasons.TARGETING_MATCH;
|
|
50
|
+
}
|
|
51
|
+
export function resolveMissingFlagEvaluation(params) {
|
|
52
|
+
params.logger?.warn(`Feature flag (${params.flagKey}) was not found.`);
|
|
53
|
+
return {
|
|
54
|
+
value: params.defaultValue,
|
|
55
|
+
reason: StandardResolutionReasons.ERROR,
|
|
56
|
+
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
|
57
|
+
errorMessage: `Feature flag (${params.flagKey}) was not found.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function resolveTypeMismatchEvaluation(params) {
|
|
61
|
+
const message = `Feature flag (${params.actualFlag.name}) resolved as ${params.actualFlag.type}, expected ${params.expectedType}.`;
|
|
62
|
+
params.logger?.warn(message);
|
|
63
|
+
return {
|
|
64
|
+
value: params.defaultValue,
|
|
65
|
+
variant: params.actualFlag.name,
|
|
66
|
+
flagMetadata: createOpenFeatureMetadata({ flag: params.actualFlag }),
|
|
67
|
+
reason: StandardResolutionReasons.ERROR,
|
|
68
|
+
errorCode: ErrorCode.TYPE_MISMATCH,
|
|
69
|
+
errorMessage: message,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function matchesExpectedType(flag, expectedType) {
|
|
73
|
+
return flag.type === expectedType;
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { OpenFeatureEventEmitter, type EvaluationContext, type JsonValue, type Provider, type ProviderMetadata, type ResolutionDetails } from "@openfeature/web-sdk";
|
|
2
|
+
import { MergedFeatureFlags } from "../client.js";
|
|
3
|
+
import type { FlagRegistry } from "../types.js";
|
|
4
|
+
import { type OpenFeatureAdapterConfig } from "./shared.js";
|
|
5
|
+
export type OpenFeatureWebProviderConfig<TFlags extends FlagRegistry = FlagRegistry> = OpenFeatureAdapterConfig & {
|
|
6
|
+
client?: MergedFeatureFlags<TFlags>;
|
|
7
|
+
};
|
|
8
|
+
export declare class MergedOpenFeatureWebProvider<TFlags extends FlagRegistry = FlagRegistry> implements Provider {
|
|
9
|
+
readonly events: OpenFeatureEventEmitter;
|
|
10
|
+
readonly metadata: ProviderMetadata;
|
|
11
|
+
readonly runsOn: "client";
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly ownsClient;
|
|
14
|
+
private readonly config;
|
|
15
|
+
private currentFlagsSignature;
|
|
16
|
+
private unsubscribe;
|
|
17
|
+
constructor(config: OpenFeatureWebProviderConfig<TFlags>);
|
|
18
|
+
initialize(context?: EvaluationContext): Promise<void>;
|
|
19
|
+
onClose(): Promise<void>;
|
|
20
|
+
onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void>;
|
|
21
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context: EvaluationContext): ResolutionDetails<boolean>;
|
|
22
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext): ResolutionDetails<string>;
|
|
23
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext): ResolutionDetails<number>;
|
|
24
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext): ResolutionDetails<T>;
|
|
25
|
+
private resolveEvaluation;
|
|
26
|
+
}
|
|
27
|
+
export declare function createOpenFeatureWebProvider<TFlags extends FlagRegistry = FlagRegistry>(config: OpenFeatureWebProviderConfig<TFlags>): MergedOpenFeatureWebProvider<TFlags>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ClientProviderEvents, OpenFeatureEventEmitter, StandardResolutionReasons, } from "@openfeature/web-sdk";
|
|
2
|
+
import { MergedFeatureFlags } from "../client.js";
|
|
3
|
+
import { findEvaluatedFlag, matchesExpectedType, resolveMissingFlagEvaluation, resolveOpenFeatureContext, resolveSuccessfulEvaluation, resolveTypeMismatchEvaluation, } from "./shared.js";
|
|
4
|
+
export class MergedOpenFeatureWebProvider {
|
|
5
|
+
events = new OpenFeatureEventEmitter();
|
|
6
|
+
metadata;
|
|
7
|
+
runsOn = "client";
|
|
8
|
+
client;
|
|
9
|
+
ownsClient;
|
|
10
|
+
config;
|
|
11
|
+
currentFlagsSignature = "";
|
|
12
|
+
unsubscribe = null;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.ownsClient = !config.client;
|
|
16
|
+
this.client =
|
|
17
|
+
config.client ??
|
|
18
|
+
new MergedFeatureFlags({
|
|
19
|
+
apiUrl: config.apiUrl,
|
|
20
|
+
clientKey: config.clientKey,
|
|
21
|
+
environmentId: config.environmentId,
|
|
22
|
+
organizationId: config.organizationId,
|
|
23
|
+
teamId: config.teamId,
|
|
24
|
+
publicKey: config.publicKey,
|
|
25
|
+
evaluationContext: config.evaluationContext,
|
|
26
|
+
refreshInterval: config.refreshInterval,
|
|
27
|
+
});
|
|
28
|
+
this.metadata = {
|
|
29
|
+
name: config.metadataName ?? "@mergedapp/feature-flags/openfeature-web",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async initialize(context) {
|
|
33
|
+
if (context) {
|
|
34
|
+
this.client.setEvaluationContext(resolveOpenFeatureContext({ config: this.config, context }));
|
|
35
|
+
}
|
|
36
|
+
await this.client.initialize();
|
|
37
|
+
let previousFlags = this.client.getAllFlags();
|
|
38
|
+
this.currentFlagsSignature = serializeFlags(previousFlags);
|
|
39
|
+
this.unsubscribe = this.client.subscribe(() => {
|
|
40
|
+
const nextFlags = this.client.getAllFlags();
|
|
41
|
+
const nextSignature = serializeFlags(nextFlags);
|
|
42
|
+
if (nextSignature !== this.currentFlagsSignature) {
|
|
43
|
+
this.currentFlagsSignature = nextSignature;
|
|
44
|
+
this.events.emit(ClientProviderEvents.ConfigurationChanged, {
|
|
45
|
+
flagsChanged: getChangedFlagNames(previousFlags, nextFlags),
|
|
46
|
+
});
|
|
47
|
+
previousFlags = nextFlags;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async onClose() {
|
|
52
|
+
this.unsubscribe?.();
|
|
53
|
+
this.unsubscribe = null;
|
|
54
|
+
if (this.ownsClient) {
|
|
55
|
+
this.client.destroy();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async onContextChange(_oldContext, newContext) {
|
|
59
|
+
this.client.setEvaluationContext(resolveOpenFeatureContext({ config: this.config, context: newContext }));
|
|
60
|
+
await this.client.refresh();
|
|
61
|
+
}
|
|
62
|
+
resolveBooleanEvaluation(flagKey, defaultValue, _context) {
|
|
63
|
+
return this.resolveEvaluation({
|
|
64
|
+
defaultValue,
|
|
65
|
+
expectedType: "BOOLEAN",
|
|
66
|
+
flagKey,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
resolveStringEvaluation(flagKey, defaultValue, _context) {
|
|
70
|
+
return this.resolveEvaluation({
|
|
71
|
+
defaultValue,
|
|
72
|
+
expectedType: "STRING",
|
|
73
|
+
flagKey,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
resolveNumberEvaluation(flagKey, defaultValue, _context) {
|
|
77
|
+
return this.resolveEvaluation({
|
|
78
|
+
defaultValue,
|
|
79
|
+
expectedType: "NUMBER",
|
|
80
|
+
flagKey,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
resolveObjectEvaluation(flagKey, defaultValue, _context) {
|
|
84
|
+
return this.resolveEvaluation({
|
|
85
|
+
defaultValue,
|
|
86
|
+
expectedType: "OBJECT",
|
|
87
|
+
flagKey,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
resolveEvaluation(params) {
|
|
91
|
+
const flag = findEvaluatedFlag(this.client.getAllFlags(), params.flagKey, this.config.flagIds);
|
|
92
|
+
if (!flag) {
|
|
93
|
+
return {
|
|
94
|
+
...resolveMissingFlagEvaluation(params),
|
|
95
|
+
reason: this.client.getStatus().source === "defaults" ? StandardResolutionReasons.DEFAULT : undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!matchesExpectedType(flag, params.expectedType)) {
|
|
99
|
+
return resolveTypeMismatchEvaluation({
|
|
100
|
+
actualFlag: flag,
|
|
101
|
+
defaultValue: params.defaultValue,
|
|
102
|
+
expectedType: params.expectedType,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...resolveSuccessfulEvaluation({
|
|
107
|
+
defaultValue: params.defaultValue,
|
|
108
|
+
flag,
|
|
109
|
+
runtimeStatus: this.client.getStatus(),
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function serializeFlags(flags) {
|
|
115
|
+
return JSON.stringify(flags.map((flag) => ({
|
|
116
|
+
enabled: flag.enabled,
|
|
117
|
+
id: flag.id,
|
|
118
|
+
name: flag.name,
|
|
119
|
+
type: flag.type,
|
|
120
|
+
value: flag.value,
|
|
121
|
+
})));
|
|
122
|
+
}
|
|
123
|
+
function getChangedFlagNames(previousFlags, nextFlags) {
|
|
124
|
+
const previousById = new Map(previousFlags.map((flag) => [flag.id, serializeComparableFlag(flag)]));
|
|
125
|
+
const nextById = new Map(nextFlags.map((flag) => [flag.id, serializeComparableFlag(flag)]));
|
|
126
|
+
const changed = new Set();
|
|
127
|
+
for (const [id, nextEntry] of nextById) {
|
|
128
|
+
const previousEntry = previousById.get(id);
|
|
129
|
+
if (!previousEntry ||
|
|
130
|
+
previousEntry.flag.enabled !== nextEntry.flag.enabled ||
|
|
131
|
+
previousEntry.flag.name !== nextEntry.flag.name ||
|
|
132
|
+
previousEntry.serializedValue !== nextEntry.serializedValue) {
|
|
133
|
+
changed.add(nextEntry.flag.name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const [id, previousEntry] of previousById) {
|
|
137
|
+
if (!nextById.has(id)) {
|
|
138
|
+
changed.add(previousEntry.flag.name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Array.from(changed);
|
|
142
|
+
}
|
|
143
|
+
function serializeComparableFlag(flag) {
|
|
144
|
+
return {
|
|
145
|
+
flag,
|
|
146
|
+
serializedValue: JSON.stringify(flag.value),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function createOpenFeatureWebProvider(config) {
|
|
150
|
+
return new MergedOpenFeatureWebProvider(config);
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./cjs/openfeature/web.js")
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createLoggingHook, createStaticContextHook } from "./hooks.js";
|
|
2
|
+
export { DEFAULT_TARGETING_KEY_ATTRIBUTE, createDefaultOpenFeatureContextMapper, mergeFeatureFlagEvaluationContexts, type OpenFeatureContextMapper, } from "./context.js";
|
|
3
|
+
export { createOpenFeatureWebProvider, MergedOpenFeatureWebProvider, type OpenFeatureWebProviderConfig, } from "./web-provider.js";
|