@quonfig/openfeature-web 0.0.5 → 0.0.7
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 +16 -16
- package/dist/index.js +58 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
OpenFeature provider for [Quonfig](https://quonfig.com) — Web/Browser.
|
|
4
4
|
|
|
5
|
-
Works with both vanilla JS (`@openfeature/web-sdk`) and React (`@openfeature/react-sdk`).
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Works with both vanilla JS (`@openfeature/web-sdk`) and React (`@openfeature/react-sdk`). The React
|
|
6
|
+
SDK re-exports the web SDK and adds hooks (`useFlag`, `useBooleanFlagValue`, etc.) — any web
|
|
7
|
+
provider works with React hooks automatically.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -68,24 +68,24 @@ function App() {
|
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
70
|
const provider = new QuonfigWebProvider({
|
|
71
|
-
sdkKey: "qf_sk_...",
|
|
72
|
-
targetingKeyMapping: "user.id",
|
|
73
|
-
apiUrl: "https://custom.api.com",
|
|
74
|
-
timeout: 5000,
|
|
71
|
+
sdkKey: "qf_sk_...", // required
|
|
72
|
+
targetingKeyMapping: "user.id", // default; maps OpenFeature targetingKey
|
|
73
|
+
apiUrl: "https://custom.api.com", // optional — override API base URL
|
|
74
|
+
timeout: 5000, // optional — request timeout in ms
|
|
75
75
|
});
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
## Context mapping
|
|
79
79
|
|
|
80
|
-
OpenFeature uses a flat context; Quonfig uses a namespace-nested context.
|
|
81
|
-
|
|
80
|
+
OpenFeature uses a flat context; Quonfig uses a namespace-nested context. The provider maps between
|
|
81
|
+
them using dot-notation:
|
|
82
82
|
|
|
83
|
-
| OpenFeature key | Quonfig context
|
|
84
|
-
|
|
85
|
-
| `targetingKey: "u-123"` | `{ user: { id: "u-123" } }`
|
|
83
|
+
| OpenFeature key | Quonfig context |
|
|
84
|
+
| ----------------------- | ------------------------------ |
|
|
85
|
+
| `targetingKey: "u-123"` | `{ user: { id: "u-123" } }` |
|
|
86
86
|
| `"user.email": "a@b.c"` | `{ user: { email: "a@b.c" } }` |
|
|
87
|
-
| `"org.tier": "pro"` | `{ org: { tier: "pro" } }`
|
|
88
|
-
| `"country": "US"` | `{ "": { country: "US" } }`
|
|
87
|
+
| `"org.tier": "pro"` | `{ org: { tier: "pro" } }` |
|
|
88
|
+
| `"country": "US"` | `{ "": { country: "US" } }` |
|
|
89
89
|
|
|
90
90
|
Keys without a dot go into the default (empty-string) namespace.
|
|
91
91
|
|
|
@@ -97,8 +97,8 @@ new QuonfigWebProvider({ sdkKey: "...", targetingKeyMapping: "account.id" });
|
|
|
97
97
|
|
|
98
98
|
## What you lose vs. the native SDK
|
|
99
99
|
|
|
100
|
-
The OpenFeature interface covers boolean, string, number, and object types.
|
|
101
|
-
|
|
100
|
+
The OpenFeature interface covers boolean, string, number, and object types. Some Quonfig-native
|
|
101
|
+
features require `provider.getClient()` (the escape hatch):
|
|
102
102
|
|
|
103
103
|
1. **Log levels** (`shouldLog`, `logger`) — native SDK only
|
|
104
104
|
2. **`string_list` configs** — access via `getObjectValue` and cast to `string[]`
|
package/dist/index.js
CHANGED
|
@@ -122,34 +122,46 @@ var QuonfigWebProvider = class {
|
|
|
122
122
|
// Private helpers
|
|
123
123
|
// ---------------------------------------------------------------------------
|
|
124
124
|
_resolve(flagKey, defaultValue, expectedType) {
|
|
125
|
+
let details;
|
|
125
126
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
value: defaultValue,
|
|
130
|
-
reason: import_web_sdk2.StandardResolutionReasons.DEFAULT,
|
|
131
|
-
errorCode: import_web_sdk2.ErrorCode.FLAG_NOT_FOUND
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const coerced = this._coerce(raw, expectedType, defaultValue);
|
|
135
|
-
if (coerced === null) {
|
|
136
|
-
return {
|
|
137
|
-
value: defaultValue,
|
|
138
|
-
reason: import_web_sdk2.StandardResolutionReasons.ERROR,
|
|
139
|
-
errorCode: import_web_sdk2.ErrorCode.TYPE_MISMATCH
|
|
140
|
-
};
|
|
141
|
-
}
|
|
127
|
+
details = this.client.getDetails(flagKey);
|
|
128
|
+
} catch (err) {
|
|
142
129
|
return {
|
|
143
|
-
value:
|
|
144
|
-
reason: import_web_sdk2.StandardResolutionReasons.
|
|
130
|
+
value: defaultValue,
|
|
131
|
+
reason: import_web_sdk2.StandardResolutionReasons.ERROR,
|
|
132
|
+
errorCode: toErrorCode(err),
|
|
133
|
+
variant: "default",
|
|
134
|
+
flagMetadata: {}
|
|
145
135
|
};
|
|
146
|
-
}
|
|
136
|
+
}
|
|
137
|
+
if (details.reason === "ERROR") {
|
|
138
|
+
return {
|
|
139
|
+
value: defaultValue,
|
|
140
|
+
reason: import_web_sdk2.StandardResolutionReasons.ERROR,
|
|
141
|
+
errorCode: toOFErrorCode(details.errorCode),
|
|
142
|
+
...details.errorMessage ? { errorMessage: details.errorMessage } : {},
|
|
143
|
+
variant: details.variant,
|
|
144
|
+
flagMetadata: details.flagMetadata
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const coerced = this._coerce(details.value, expectedType, defaultValue);
|
|
148
|
+
if (coerced === null) {
|
|
147
149
|
return {
|
|
148
150
|
value: defaultValue,
|
|
149
151
|
reason: import_web_sdk2.StandardResolutionReasons.ERROR,
|
|
150
|
-
errorCode:
|
|
152
|
+
errorCode: import_web_sdk2.ErrorCode.TYPE_MISMATCH,
|
|
153
|
+
// OF spec convention: variant='default' on the error path; preserve
|
|
154
|
+
// flagMetadata so consumers still see configId/configType.
|
|
155
|
+
variant: "default",
|
|
156
|
+
flagMetadata: details.flagMetadata
|
|
151
157
|
};
|
|
152
158
|
}
|
|
159
|
+
return {
|
|
160
|
+
value: coerced,
|
|
161
|
+
reason: toOFReason(details.reason),
|
|
162
|
+
variant: details.variant,
|
|
163
|
+
flagMetadata: details.flagMetadata
|
|
164
|
+
};
|
|
153
165
|
}
|
|
154
166
|
/**
|
|
155
167
|
* Coerce a raw ConfigValue to the expected OF type.
|
|
@@ -190,6 +202,32 @@ var QuonfigWebProvider = class {
|
|
|
190
202
|
return iso;
|
|
191
203
|
}
|
|
192
204
|
};
|
|
205
|
+
function toOFReason(reason) {
|
|
206
|
+
switch (reason) {
|
|
207
|
+
case "STATIC":
|
|
208
|
+
return import_web_sdk2.StandardResolutionReasons.STATIC;
|
|
209
|
+
case "TARGETING_MATCH":
|
|
210
|
+
return import_web_sdk2.StandardResolutionReasons.TARGETING_MATCH;
|
|
211
|
+
case "SPLIT":
|
|
212
|
+
return import_web_sdk2.StandardResolutionReasons.SPLIT;
|
|
213
|
+
case "DEFAULT":
|
|
214
|
+
return import_web_sdk2.StandardResolutionReasons.DEFAULT;
|
|
215
|
+
case "ERROR":
|
|
216
|
+
default:
|
|
217
|
+
return import_web_sdk2.StandardResolutionReasons.ERROR;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function toOFErrorCode(code) {
|
|
221
|
+
switch (code) {
|
|
222
|
+
case "FLAG_NOT_FOUND":
|
|
223
|
+
return import_web_sdk2.ErrorCode.FLAG_NOT_FOUND;
|
|
224
|
+
case "TYPE_MISMATCH":
|
|
225
|
+
return import_web_sdk2.ErrorCode.TYPE_MISMATCH;
|
|
226
|
+
case "GENERAL":
|
|
227
|
+
default:
|
|
228
|
+
return import_web_sdk2.ErrorCode.GENERAL;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
193
231
|
// Annotate the CommonJS export names for ESM import in node:
|
|
194
232
|
0 && (module.exports = {
|
|
195
233
|
QuonfigWebProvider,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["export { QuonfigWebProvider } from \"./provider\";\nexport type { QuonfigWebProviderOptions } from \"./provider\";\nexport { mapContext } from \"./context\";\nexport { toErrorCode } from \"./errors\";\n","import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context\n ? mapContext(context, this.targetingKeyMapping)\n : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(\n _oldCtx: EvaluationContext,\n newCtx: EvaluationContext\n ): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n try {\n const raw = this.client.get(flagKey);\n\n if (raw === undefined || raw === null) {\n // Flag not found — return OF default\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.DEFAULT,\n errorCode: ErrorCode.FLAG_NOT_FOUND,\n };\n }\n\n // Type coercion / validation\n const coerced = this._coerce<T>(raw, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n };\n }\n\n return {\n value: coerced,\n reason: StandardResolutionReasons.STATIC,\n };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n };\n }\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (\n raw !== null &&\n typeof raw === \"object\" &&\n \"seconds\" in raw &&\n \"ms\" in raw\n ) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop =\n dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["import_web_sdk","dotIdx"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["export { QuonfigWebProvider } from \"./provider\";\nexport type { QuonfigWebProviderOptions } from \"./provider\";\nexport { mapContext } from \"./context\";\nexport { toErrorCode } from \"./errors\";\n","import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue, ResolutionReason } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\nimport type { EvaluationDetails } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context ? mapContext(context, this.targetingKeyMapping) : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n let details: EvaluationDetails;\n try {\n details = this.client.getDetails(flagKey);\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n variant: \"default\",\n flagMetadata: {},\n };\n }\n\n // Errors from the SDK side (FLAG_NOT_FOUND, GENERAL) — pass through.\n if (details.reason === \"ERROR\") {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n ...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n // Coerce the resolved value to the expected OF shape. A null result\n // signals provider-level TYPE_MISMATCH (the SDK has no requested-type\n // hint, so this happens here, not inside getDetails).\n const coerced = this._coerce<T>(details.value, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n // OF spec convention: variant='default' on the error path; preserve\n // flagMetadata so consumers still see configId/configType.\n variant: \"default\",\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n return {\n value: coerced,\n reason: toOFReason(details.reason),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (raw !== null && typeof raw === \"object\" && \"seconds\" in raw && \"ms\" in raw) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Map sdk-javascript's EvaluationReason onto OpenFeature's StandardResolutionReasons.\n * SPLIT is provider-defined; OF allows arbitrary strings, so the literal value works.\n */\nfunction toOFReason(reason: EvaluationDetails[\"reason\"]): ResolutionReason {\n switch (reason) {\n case \"STATIC\":\n return StandardResolutionReasons.STATIC;\n case \"TARGETING_MATCH\":\n return StandardResolutionReasons.TARGETING_MATCH;\n case \"SPLIT\":\n return StandardResolutionReasons.SPLIT;\n case \"DEFAULT\":\n return StandardResolutionReasons.DEFAULT;\n case \"ERROR\":\n default:\n return StandardResolutionReasons.ERROR;\n }\n}\n\n/** Translate sdk-javascript's EvaluationErrorCode onto the OF ErrorCode enum. */\nfunction toOFErrorCode(code: EvaluationDetails[\"errorCode\"]): ErrorCode {\n switch (code) {\n case \"FLAG_NOT_FOUND\":\n return ErrorCode.FLAG_NOT_FOUND;\n case \"TYPE_MISMATCH\":\n return ErrorCode.TYPE_MISMATCH;\n case \"GENERAL\":\n default:\n return ErrorCode.GENERAL;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop = dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["import_web_sdk","dotIdx"]}
|
package/dist/index.mjs
CHANGED
|
@@ -98,34 +98,46 @@ var QuonfigWebProvider = class {
|
|
|
98
98
|
// Private helpers
|
|
99
99
|
// ---------------------------------------------------------------------------
|
|
100
100
|
_resolve(flagKey, defaultValue, expectedType) {
|
|
101
|
+
let details;
|
|
101
102
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
value: defaultValue,
|
|
106
|
-
reason: StandardResolutionReasons.DEFAULT,
|
|
107
|
-
errorCode: ErrorCode2.FLAG_NOT_FOUND
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
const coerced = this._coerce(raw, expectedType, defaultValue);
|
|
111
|
-
if (coerced === null) {
|
|
112
|
-
return {
|
|
113
|
-
value: defaultValue,
|
|
114
|
-
reason: StandardResolutionReasons.ERROR,
|
|
115
|
-
errorCode: ErrorCode2.TYPE_MISMATCH
|
|
116
|
-
};
|
|
117
|
-
}
|
|
103
|
+
details = this.client.getDetails(flagKey);
|
|
104
|
+
} catch (err) {
|
|
118
105
|
return {
|
|
119
|
-
value:
|
|
120
|
-
reason: StandardResolutionReasons.
|
|
106
|
+
value: defaultValue,
|
|
107
|
+
reason: StandardResolutionReasons.ERROR,
|
|
108
|
+
errorCode: toErrorCode(err),
|
|
109
|
+
variant: "default",
|
|
110
|
+
flagMetadata: {}
|
|
121
111
|
};
|
|
122
|
-
}
|
|
112
|
+
}
|
|
113
|
+
if (details.reason === "ERROR") {
|
|
114
|
+
return {
|
|
115
|
+
value: defaultValue,
|
|
116
|
+
reason: StandardResolutionReasons.ERROR,
|
|
117
|
+
errorCode: toOFErrorCode(details.errorCode),
|
|
118
|
+
...details.errorMessage ? { errorMessage: details.errorMessage } : {},
|
|
119
|
+
variant: details.variant,
|
|
120
|
+
flagMetadata: details.flagMetadata
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const coerced = this._coerce(details.value, expectedType, defaultValue);
|
|
124
|
+
if (coerced === null) {
|
|
123
125
|
return {
|
|
124
126
|
value: defaultValue,
|
|
125
127
|
reason: StandardResolutionReasons.ERROR,
|
|
126
|
-
errorCode:
|
|
128
|
+
errorCode: ErrorCode2.TYPE_MISMATCH,
|
|
129
|
+
// OF spec convention: variant='default' on the error path; preserve
|
|
130
|
+
// flagMetadata so consumers still see configId/configType.
|
|
131
|
+
variant: "default",
|
|
132
|
+
flagMetadata: details.flagMetadata
|
|
127
133
|
};
|
|
128
134
|
}
|
|
135
|
+
return {
|
|
136
|
+
value: coerced,
|
|
137
|
+
reason: toOFReason(details.reason),
|
|
138
|
+
variant: details.variant,
|
|
139
|
+
flagMetadata: details.flagMetadata
|
|
140
|
+
};
|
|
129
141
|
}
|
|
130
142
|
/**
|
|
131
143
|
* Coerce a raw ConfigValue to the expected OF type.
|
|
@@ -166,6 +178,32 @@ var QuonfigWebProvider = class {
|
|
|
166
178
|
return iso;
|
|
167
179
|
}
|
|
168
180
|
};
|
|
181
|
+
function toOFReason(reason) {
|
|
182
|
+
switch (reason) {
|
|
183
|
+
case "STATIC":
|
|
184
|
+
return StandardResolutionReasons.STATIC;
|
|
185
|
+
case "TARGETING_MATCH":
|
|
186
|
+
return StandardResolutionReasons.TARGETING_MATCH;
|
|
187
|
+
case "SPLIT":
|
|
188
|
+
return StandardResolutionReasons.SPLIT;
|
|
189
|
+
case "DEFAULT":
|
|
190
|
+
return StandardResolutionReasons.DEFAULT;
|
|
191
|
+
case "ERROR":
|
|
192
|
+
default:
|
|
193
|
+
return StandardResolutionReasons.ERROR;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function toOFErrorCode(code) {
|
|
197
|
+
switch (code) {
|
|
198
|
+
case "FLAG_NOT_FOUND":
|
|
199
|
+
return ErrorCode2.FLAG_NOT_FOUND;
|
|
200
|
+
case "TYPE_MISMATCH":
|
|
201
|
+
return ErrorCode2.TYPE_MISMATCH;
|
|
202
|
+
case "GENERAL":
|
|
203
|
+
default:
|
|
204
|
+
return ErrorCode2.GENERAL;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
169
207
|
export {
|
|
170
208
|
QuonfigWebProvider,
|
|
171
209
|
mapContext,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context\n ? mapContext(context, this.targetingKeyMapping)\n : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(\n _oldCtx: EvaluationContext,\n newCtx: EvaluationContext\n ): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n try {\n const raw = this.client.get(flagKey);\n\n if (raw === undefined || raw === null) {\n // Flag not found — return OF default\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.DEFAULT,\n errorCode: ErrorCode.FLAG_NOT_FOUND,\n };\n }\n\n // Type coercion / validation\n const coerced = this._coerce<T>(raw, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n };\n }\n\n return {\n value: coerced,\n reason: StandardResolutionReasons.STATIC,\n };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n };\n }\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (\n raw !== null &&\n typeof raw === \"object\" &&\n \"seconds\" in raw &&\n \"ms\" in raw\n ) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop =\n dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["ErrorCode","dotIdx","ErrorCode"]}
|
|
1
|
+
{"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue, ResolutionReason } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\nimport type { EvaluationDetails } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context ? mapContext(context, this.targetingKeyMapping) : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n let details: EvaluationDetails;\n try {\n details = this.client.getDetails(flagKey);\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n variant: \"default\",\n flagMetadata: {},\n };\n }\n\n // Errors from the SDK side (FLAG_NOT_FOUND, GENERAL) — pass through.\n if (details.reason === \"ERROR\") {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n ...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n // Coerce the resolved value to the expected OF shape. A null result\n // signals provider-level TYPE_MISMATCH (the SDK has no requested-type\n // hint, so this happens here, not inside getDetails).\n const coerced = this._coerce<T>(details.value, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n // OF spec convention: variant='default' on the error path; preserve\n // flagMetadata so consumers still see configId/configType.\n variant: \"default\",\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n return {\n value: coerced,\n reason: toOFReason(details.reason),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (raw !== null && typeof raw === \"object\" && \"seconds\" in raw && \"ms\" in raw) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Map sdk-javascript's EvaluationReason onto OpenFeature's StandardResolutionReasons.\n * SPLIT is provider-defined; OF allows arbitrary strings, so the literal value works.\n */\nfunction toOFReason(reason: EvaluationDetails[\"reason\"]): ResolutionReason {\n switch (reason) {\n case \"STATIC\":\n return StandardResolutionReasons.STATIC;\n case \"TARGETING_MATCH\":\n return StandardResolutionReasons.TARGETING_MATCH;\n case \"SPLIT\":\n return StandardResolutionReasons.SPLIT;\n case \"DEFAULT\":\n return StandardResolutionReasons.DEFAULT;\n case \"ERROR\":\n default:\n return StandardResolutionReasons.ERROR;\n }\n}\n\n/** Translate sdk-javascript's EvaluationErrorCode onto the OF ErrorCode enum. */\nfunction toOFErrorCode(code: EvaluationDetails[\"errorCode\"]): ErrorCode {\n switch (code) {\n case \"FLAG_NOT_FOUND\":\n return ErrorCode.FLAG_NOT_FOUND;\n case \"TYPE_MISMATCH\":\n return ErrorCode.TYPE_MISMATCH;\n case \"GENERAL\":\n default:\n return ErrorCode.GENERAL;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop = dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["ErrorCode","dotIdx","ErrorCode"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quonfig/openfeature-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "OpenFeature provider for Quonfig — Web/Browser (also works with @openfeature/react-sdk)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -20,16 +20,20 @@
|
|
|
20
20
|
"test": "vitest run --exclude test/conformance/**",
|
|
21
21
|
"test:conformance": "vitest run test/conformance",
|
|
22
22
|
"lint": "tsc --noEmit",
|
|
23
|
-
"prepublishOnly": "npm run build"
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"prettier": "prettier . -l",
|
|
25
|
+
"prettier:fix": "prettier --write .",
|
|
26
|
+
"version": "prettier --write CHANGELOG.md README.md && git add CHANGELOG.md README.md"
|
|
24
27
|
},
|
|
25
28
|
"peerDependencies": {
|
|
26
|
-
"@quonfig/javascript": ">=0.0.
|
|
29
|
+
"@quonfig/javascript": ">=0.0.16",
|
|
27
30
|
"@openfeature/web-sdk": ">=1.0.0"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
|
-
"@quonfig/javascript": "^0.0.
|
|
33
|
+
"@quonfig/javascript": "^0.0.16",
|
|
31
34
|
"@openfeature/web-sdk": "^1.0.0",
|
|
32
35
|
"@types/node": "^20.11.0",
|
|
36
|
+
"prettier": "^3.0.0",
|
|
33
37
|
"tsup": "^8.0.0",
|
|
34
38
|
"typescript": "^5.3.0",
|
|
35
39
|
"vitest": "^1.0.0"
|