@quonfig/openfeature-node 0.0.4 → 0.0.6

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 CHANGED
@@ -39,43 +39,39 @@ const timeout = await client.getNumberValue("request-timeout-ms", 5000);
39
39
  const allowedPlans = await client.getObjectValue("allowed-plans", []);
40
40
 
41
41
  // With evaluation context (per-request)
42
- const isProFeatureEnabled = await client.getBooleanValue(
43
- "pro-feature",
44
- false,
45
- {
46
- targetingKey: "user-123", // maps to user.id by default
47
- "user.plan": "pro",
48
- "org.tier": "enterprise",
49
- }
50
- );
42
+ const isProFeatureEnabled = await client.getBooleanValue("pro-feature", false, {
43
+ targetingKey: "user-123", // maps to user.id by default
44
+ "user.plan": "pro",
45
+ "org.tier": "enterprise",
46
+ });
51
47
  ```
52
48
 
53
49
  ## Context mapping
54
50
 
55
- OpenFeature context is flat; Quonfig context is nested by namespace. This provider
56
- maps between them using dot-notation:
51
+ OpenFeature context is flat; Quonfig context is nested by namespace. This provider maps between them
52
+ using dot-notation:
57
53
 
58
- | OpenFeature context key | Quonfig namespace | Quonfig property |
59
- |-------------------------|-------------------|-----------------|
60
- | `targetingKey` | `user` | `id` (configurable via `targetingKeyMapping`) |
61
- | `"user.email"` | `user` | `email` |
62
- | `"org.tier"` | `org` | `tier` |
63
- | `"country"` (no dot) | `""` (default) | `country` |
64
- | `"user.ip.address"` | `user` | `ip.address` (first dot only) |
54
+ | OpenFeature context key | Quonfig namespace | Quonfig property |
55
+ | ----------------------- | ----------------- | --------------------------------------------- |
56
+ | `targetingKey` | `user` | `id` (configurable via `targetingKeyMapping`) |
57
+ | `"user.email"` | `user` | `email` |
58
+ | `"org.tier"` | `org` | `tier` |
59
+ | `"country"` (no dot) | `""` (default) | `country` |
60
+ | `"user.ip.address"` | `user` | `ip.address` (first dot only) |
65
61
 
66
62
  ### Customizing targetingKey mapping
67
63
 
68
64
  ```typescript
69
65
  const provider = new QuonfigProvider({
70
66
  sdkKey: "qf_sk_...",
71
- targetingKeyMapping: "account.id", // maps targetingKey to { account: { id: ... } }
67
+ targetingKeyMapping: "account.id", // maps targetingKey to { account: { id: ... } }
72
68
  });
73
69
  ```
74
70
 
75
71
  ## Accessing native SDK features
76
72
 
77
- The `getClient()` escape hatch returns the underlying `@quonfig/node` client for
78
- features not available in OpenFeature:
73
+ The `getClient()` escape hatch returns the underlying `@quonfig/node` client for features not
74
+ available in OpenFeature:
79
75
 
80
76
  ```typescript
81
77
  const native = provider.getClient();
@@ -96,8 +92,8 @@ const rawConfig = native.rawConfig("my-flag");
96
92
 
97
93
  ## What you lose vs. the native SDK
98
94
 
99
- OpenFeature is designed for feature flags, not general configuration. Some Quonfig
100
- features require the native `@quonfig/node` SDK:
95
+ OpenFeature is designed for feature flags, not general configuration. Some Quonfig features require
96
+ the native `@quonfig/node` SDK:
101
97
 
102
98
  1. **Log levels** -- `shouldLog()` and `logger()` are native-only.
103
99
  2. **`string_list` configs** -- must be accessed via `getObjectValue()` and cast to `string[]`.
@@ -109,8 +105,8 @@ features require the native `@quonfig/node` SDK:
109
105
 
110
106
  ## Configuration changed events
111
107
 
112
- The provider emits `ProviderEvents.ConfigurationChanged` when Quonfig pushes a
113
- live config update via SSE. Register a handler on the OpenFeature API or client:
108
+ The provider emits `ProviderEvents.ConfigurationChanged` when Quonfig pushes a live config update
109
+ via SSE. Register a handler on the OpenFeature API or client:
114
110
 
115
111
  ```typescript
116
112
  OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["export { QuonfigProvider } from \"./provider.js\";\nexport type { QuonfigProviderOptions } from \"./provider.js\";\nexport { mapContext } from \"./context.js\";\nexport { toErrorCode } from \"./errors.js\";\n","import {\n ErrorCode,\n EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { EvaluationDetails, Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\n\nexport interface QuonfigProviderOptions extends Omit<QuonfigOptions, \"onConfigUpdate\"> {\n /**\n * Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.\n * Defaults to \"user.id\".\n */\n targetingKeyMapping?: string;\n}\n\n/**\n * QuonfigProvider wraps the @quonfig/node native SDK and implements the\n * OpenFeature server-side Provider interface.\n *\n * Usage:\n * ```typescript\n * import { QuonfigProvider } from \"@quonfig/openfeature-node\";\n * import { OpenFeature } from \"@openfeature/server-sdk\";\n *\n * const provider = new QuonfigProvider({ sdkKey: \"qf_sk_...\" });\n * await OpenFeature.setProviderAndWait(provider);\n * const client = OpenFeature.getClient();\n * const enabled = await client.getBooleanValue(\"my-flag\", false);\n * ```\n */\nexport class QuonfigProvider implements Provider {\n readonly metadata = { name: \"quonfig\" } as const;\n readonly events = new OpenFeatureEventEmitter();\n readonly hooks = [];\n\n private readonly client: Quonfig;\n private readonly targetingKeyMapping: string;\n\n constructor(options: QuonfigProviderOptions) {\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n });\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n async resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<boolean>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getBoolDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<string>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getStringDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<number>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getNumberDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveObjectEvaluation<T extends JsonValue>(\n flagKey: string,\n defaultValue: T,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<T>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n // Try string_list first (returns string[]), fall back to JSON.\n const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);\n if (\n listDetails.reason === \"STATIC\" ||\n listDetails.reason === \"TARGETING_MATCH\" ||\n listDetails.reason === \"SPLIT\"\n ) {\n return toResolutionDetails(\n listDetails as EvaluationDetails<unknown> as EvaluationDetails<T>,\n defaultValue,\n );\n }\n const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);\n return toResolutionDetails(jsonDetails as EvaluationDetails<T>, defaultValue);\n }\n\n /**\n * Escape hatch: access the underlying @quonfig/node client for native-only features\n * (shouldLog, keys, rawConfig, etc.)\n */\n getClient(): Quonfig {\n return this.client;\n }\n}\n\n/**\n * Translate a Quonfig EvaluationDetails<T> into an OpenFeature ResolutionDetails<T>.\n *\n * Reason mapping (from the brief):\n * STATIC → StandardResolutionReasons.STATIC\n * TARGETING_MATCH → StandardResolutionReasons.TARGETING_MATCH\n * SPLIT → StandardResolutionReasons.SPLIT\n * DEFAULT → defaultValue + StandardResolutionReasons.DEFAULT\n * ERROR → defaultValue + ERROR + errorCode (FLAG_NOT_FOUND / TYPE_MISMATCH / GENERAL)\n */\nfunction toResolutionDetails<T>(\n details: EvaluationDetails<T>,\n defaultValue: T,\n): ResolutionDetails<T> {\n switch (details.reason) {\n case \"STATIC\":\n return { value: details.value as T, reason: StandardResolutionReasons.STATIC };\n case \"TARGETING_MATCH\":\n return {\n value: details.value as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n case \"SPLIT\":\n return { value: details.value as T, reason: StandardResolutionReasons.SPLIT };\n case \"DEFAULT\":\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n case \"ERROR\":\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n };\n }\n}\n\nfunction toOFErrorCode(code: EvaluationDetails<unknown>[\"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/server-sdk\";\nimport type { Contexts, ContextValue } from \"@quonfig/node\";\n\n/**\n * Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the first dot: \"user.email\" -> namespace \"user\", key \"email\"\n * - Keys without a dot go to the default (empty-string) namespace: \"country\" -> { \"\": { country: ... } }\n * - Multi-dot keys split on first dot only: \"user.ip.address\" -> { user: { \"ip.address\": ... } }\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\",\n): Contexts {\n const result: Record<string, Record<string, ContextValue>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n // Cast to ContextValue -- OpenFeature allows arbitrary nesting but Quonfig\n // contexts accept primitives and string arrays. Callers should pass only\n // primitive or string[] values for keys they want evaluated.\n const ctxValue = value as ContextValue;\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] = ctxValue;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = ctxValue;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = ctxValue;\n }\n }\n\n return result;\n}\n","import { ErrorCode } from \"@openfeature/server-sdk\";\n\n/**\n * Maps a native SDK error to an OpenFeature ErrorCode.\n *\n * The native SDK throws Error instances with message strings. We map by inspecting\n * the lowercased message.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (\n msg.includes(\"not found\") ||\n msg.includes(\"flag not found\") ||\n msg.includes(\"no value found\") ||\n msg.includes(\"value found for key\")\n ) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (msg.includes(\"not initialized\") || msg.includes(\"provider not ready\")) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["dotIdx","import_server_sdk"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["export { QuonfigProvider } from \"./provider.js\";\nexport type { QuonfigProviderOptions } from \"./provider.js\";\nexport { mapContext } from \"./context.js\";\nexport { toErrorCode } from \"./errors.js\";\n","import {\n ErrorCode,\n EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { EvaluationDetails, Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\n\nexport interface QuonfigProviderOptions extends Omit<QuonfigOptions, \"onConfigUpdate\"> {\n /**\n * Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.\n * Defaults to \"user.id\".\n */\n targetingKeyMapping?: string;\n}\n\n/**\n * QuonfigProvider wraps the @quonfig/node native SDK and implements the\n * OpenFeature server-side Provider interface.\n *\n * Usage:\n * ```typescript\n * import { QuonfigProvider } from \"@quonfig/openfeature-node\";\n * import { OpenFeature } from \"@openfeature/server-sdk\";\n *\n * const provider = new QuonfigProvider({ sdkKey: \"qf_sk_...\" });\n * await OpenFeature.setProviderAndWait(provider);\n * const client = OpenFeature.getClient();\n * const enabled = await client.getBooleanValue(\"my-flag\", false);\n * ```\n */\nexport class QuonfigProvider implements Provider {\n readonly metadata = { name: \"quonfig\" } as const;\n readonly events = new OpenFeatureEventEmitter();\n readonly hooks = [];\n\n private readonly client: Quonfig;\n private readonly targetingKeyMapping: string;\n\n constructor(options: QuonfigProviderOptions) {\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n });\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n async resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<boolean>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getBoolDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<string>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getStringDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<number>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getNumberDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveObjectEvaluation<T extends JsonValue>(\n flagKey: string,\n defaultValue: T,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<T>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n // Try string_list first (returns string[]), fall back to JSON.\n const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);\n if (\n listDetails.reason === \"STATIC\" ||\n listDetails.reason === \"TARGETING_MATCH\" ||\n listDetails.reason === \"SPLIT\"\n ) {\n return toResolutionDetails(\n listDetails as EvaluationDetails<unknown> as EvaluationDetails<T>,\n defaultValue\n );\n }\n const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);\n return toResolutionDetails(jsonDetails as EvaluationDetails<T>, defaultValue);\n }\n\n /**\n * Escape hatch: access the underlying @quonfig/node client for native-only features\n * (shouldLog, keys, rawConfig, etc.)\n */\n getClient(): Quonfig {\n return this.client;\n }\n}\n\n/**\n * Translate a Quonfig EvaluationDetails<T> into an OpenFeature ResolutionDetails<T>.\n *\n * Reason mapping (from the brief):\n * STATIC → StandardResolutionReasons.STATIC\n * TARGETING_MATCH → StandardResolutionReasons.TARGETING_MATCH\n * SPLIT → StandardResolutionReasons.SPLIT\n * DEFAULT → defaultValue + StandardResolutionReasons.DEFAULT\n * ERROR → defaultValue + ERROR + errorCode (FLAG_NOT_FOUND / TYPE_MISMATCH / GENERAL)\n */\nfunction toResolutionDetails<T>(\n details: EvaluationDetails<T>,\n defaultValue: T\n): ResolutionDetails<T> {\n switch (details.reason) {\n case \"STATIC\":\n return { value: details.value as T, reason: StandardResolutionReasons.STATIC };\n case \"TARGETING_MATCH\":\n return {\n value: details.value as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n case \"SPLIT\":\n return { value: details.value as T, reason: StandardResolutionReasons.SPLIT };\n case \"DEFAULT\":\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n case \"ERROR\":\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n };\n }\n}\n\nfunction toOFErrorCode(code: EvaluationDetails<unknown>[\"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/server-sdk\";\nimport type { Contexts, ContextValue } from \"@quonfig/node\";\n\n/**\n * Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the first dot: \"user.email\" -> namespace \"user\", key \"email\"\n * - Keys without a dot go to the default (empty-string) namespace: \"country\" -> { \"\": { country: ... } }\n * - Multi-dot keys split on first dot only: \"user.ip.address\" -> { user: { \"ip.address\": ... } }\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, ContextValue>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n // Cast to ContextValue -- OpenFeature allows arbitrary nesting but Quonfig\n // contexts accept primitives and string arrays. Callers should pass only\n // primitive or string[] values for keys they want evaluated.\n const ctxValue = value as ContextValue;\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] = ctxValue;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = ctxValue;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = ctxValue;\n }\n }\n\n return result;\n}\n","import { ErrorCode } from \"@openfeature/server-sdk\";\n\n/**\n * Maps a native SDK error to an OpenFeature ErrorCode.\n *\n * The native SDK throws Error instances with message strings. We map by inspecting\n * the lowercased message.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (\n msg.includes(\"not found\") ||\n msg.includes(\"flag not found\") ||\n msg.includes(\"no value found\") ||\n msg.includes(\"value found for key\")\n ) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (msg.includes(\"not initialized\") || msg.includes(\"provider not ready\")) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["dotIdx","import_server_sdk"]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n ErrorCode,\n EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { EvaluationDetails, Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\n\nexport interface QuonfigProviderOptions extends Omit<QuonfigOptions, \"onConfigUpdate\"> {\n /**\n * Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.\n * Defaults to \"user.id\".\n */\n targetingKeyMapping?: string;\n}\n\n/**\n * QuonfigProvider wraps the @quonfig/node native SDK and implements the\n * OpenFeature server-side Provider interface.\n *\n * Usage:\n * ```typescript\n * import { QuonfigProvider } from \"@quonfig/openfeature-node\";\n * import { OpenFeature } from \"@openfeature/server-sdk\";\n *\n * const provider = new QuonfigProvider({ sdkKey: \"qf_sk_...\" });\n * await OpenFeature.setProviderAndWait(provider);\n * const client = OpenFeature.getClient();\n * const enabled = await client.getBooleanValue(\"my-flag\", false);\n * ```\n */\nexport class QuonfigProvider implements Provider {\n readonly metadata = { name: \"quonfig\" } as const;\n readonly events = new OpenFeatureEventEmitter();\n readonly hooks = [];\n\n private readonly client: Quonfig;\n private readonly targetingKeyMapping: string;\n\n constructor(options: QuonfigProviderOptions) {\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n });\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n async resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<boolean>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getBoolDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<string>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getStringDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<number>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getNumberDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveObjectEvaluation<T extends JsonValue>(\n flagKey: string,\n defaultValue: T,\n context: EvaluationContext,\n _logger: Logger,\n ): Promise<ResolutionDetails<T>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n // Try string_list first (returns string[]), fall back to JSON.\n const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);\n if (\n listDetails.reason === \"STATIC\" ||\n listDetails.reason === \"TARGETING_MATCH\" ||\n listDetails.reason === \"SPLIT\"\n ) {\n return toResolutionDetails(\n listDetails as EvaluationDetails<unknown> as EvaluationDetails<T>,\n defaultValue,\n );\n }\n const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);\n return toResolutionDetails(jsonDetails as EvaluationDetails<T>, defaultValue);\n }\n\n /**\n * Escape hatch: access the underlying @quonfig/node client for native-only features\n * (shouldLog, keys, rawConfig, etc.)\n */\n getClient(): Quonfig {\n return this.client;\n }\n}\n\n/**\n * Translate a Quonfig EvaluationDetails<T> into an OpenFeature ResolutionDetails<T>.\n *\n * Reason mapping (from the brief):\n * STATIC → StandardResolutionReasons.STATIC\n * TARGETING_MATCH → StandardResolutionReasons.TARGETING_MATCH\n * SPLIT → StandardResolutionReasons.SPLIT\n * DEFAULT → defaultValue + StandardResolutionReasons.DEFAULT\n * ERROR → defaultValue + ERROR + errorCode (FLAG_NOT_FOUND / TYPE_MISMATCH / GENERAL)\n */\nfunction toResolutionDetails<T>(\n details: EvaluationDetails<T>,\n defaultValue: T,\n): ResolutionDetails<T> {\n switch (details.reason) {\n case \"STATIC\":\n return { value: details.value as T, reason: StandardResolutionReasons.STATIC };\n case \"TARGETING_MATCH\":\n return {\n value: details.value as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n case \"SPLIT\":\n return { value: details.value as T, reason: StandardResolutionReasons.SPLIT };\n case \"DEFAULT\":\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n case \"ERROR\":\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n };\n }\n}\n\nfunction toOFErrorCode(code: EvaluationDetails<unknown>[\"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/server-sdk\";\nimport type { Contexts, ContextValue } from \"@quonfig/node\";\n\n/**\n * Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the first dot: \"user.email\" -> namespace \"user\", key \"email\"\n * - Keys without a dot go to the default (empty-string) namespace: \"country\" -> { \"\": { country: ... } }\n * - Multi-dot keys split on first dot only: \"user.ip.address\" -> { user: { \"ip.address\": ... } }\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\",\n): Contexts {\n const result: Record<string, Record<string, ContextValue>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n // Cast to ContextValue -- OpenFeature allows arbitrary nesting but Quonfig\n // contexts accept primitives and string arrays. Callers should pass only\n // primitive or string[] values for keys they want evaluated.\n const ctxValue = value as ContextValue;\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] = ctxValue;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = ctxValue;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = ctxValue;\n }\n }\n\n return result;\n}\n","import { ErrorCode } from \"@openfeature/server-sdk\";\n\n/**\n * Maps a native SDK error to an OpenFeature ErrorCode.\n *\n * The native SDK throws Error instances with message strings. We map by inspecting\n * the lowercased message.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (\n msg.includes(\"not found\") ||\n msg.includes(\"flag not found\") ||\n msg.includes(\"no value found\") ||\n msg.includes(\"value found for key\")\n ) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (msg.includes(\"not initialized\") || msg.includes(\"provider not ready\")) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["dotIdx","ErrorCode"]}
1
+ {"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n ErrorCode,\n EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { EvaluationDetails, Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\n\nexport interface QuonfigProviderOptions extends Omit<QuonfigOptions, \"onConfigUpdate\"> {\n /**\n * Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.\n * Defaults to \"user.id\".\n */\n targetingKeyMapping?: string;\n}\n\n/**\n * QuonfigProvider wraps the @quonfig/node native SDK and implements the\n * OpenFeature server-side Provider interface.\n *\n * Usage:\n * ```typescript\n * import { QuonfigProvider } from \"@quonfig/openfeature-node\";\n * import { OpenFeature } from \"@openfeature/server-sdk\";\n *\n * const provider = new QuonfigProvider({ sdkKey: \"qf_sk_...\" });\n * await OpenFeature.setProviderAndWait(provider);\n * const client = OpenFeature.getClient();\n * const enabled = await client.getBooleanValue(\"my-flag\", false);\n * ```\n */\nexport class QuonfigProvider implements Provider {\n readonly metadata = { name: \"quonfig\" } as const;\n readonly events = new OpenFeatureEventEmitter();\n readonly hooks = [];\n\n private readonly client: Quonfig;\n private readonly targetingKeyMapping: string;\n\n constructor(options: QuonfigProviderOptions) {\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n });\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n await this.client.close();\n }\n\n async resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<boolean>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getBoolDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<string>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getStringDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<number>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n const details = this.client.getNumberDetails(flagKey, mappedCtx);\n return toResolutionDetails(details, defaultValue);\n }\n\n async resolveObjectEvaluation<T extends JsonValue>(\n flagKey: string,\n defaultValue: T,\n context: EvaluationContext,\n _logger: Logger\n ): Promise<ResolutionDetails<T>> {\n const mappedCtx = mapContext(context, this.targetingKeyMapping);\n // Try string_list first (returns string[]), fall back to JSON.\n const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);\n if (\n listDetails.reason === \"STATIC\" ||\n listDetails.reason === \"TARGETING_MATCH\" ||\n listDetails.reason === \"SPLIT\"\n ) {\n return toResolutionDetails(\n listDetails as EvaluationDetails<unknown> as EvaluationDetails<T>,\n defaultValue\n );\n }\n const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);\n return toResolutionDetails(jsonDetails as EvaluationDetails<T>, defaultValue);\n }\n\n /**\n * Escape hatch: access the underlying @quonfig/node client for native-only features\n * (shouldLog, keys, rawConfig, etc.)\n */\n getClient(): Quonfig {\n return this.client;\n }\n}\n\n/**\n * Translate a Quonfig EvaluationDetails<T> into an OpenFeature ResolutionDetails<T>.\n *\n * Reason mapping (from the brief):\n * STATIC → StandardResolutionReasons.STATIC\n * TARGETING_MATCH → StandardResolutionReasons.TARGETING_MATCH\n * SPLIT → StandardResolutionReasons.SPLIT\n * DEFAULT → defaultValue + StandardResolutionReasons.DEFAULT\n * ERROR → defaultValue + ERROR + errorCode (FLAG_NOT_FOUND / TYPE_MISMATCH / GENERAL)\n */\nfunction toResolutionDetails<T>(\n details: EvaluationDetails<T>,\n defaultValue: T\n): ResolutionDetails<T> {\n switch (details.reason) {\n case \"STATIC\":\n return { value: details.value as T, reason: StandardResolutionReasons.STATIC };\n case \"TARGETING_MATCH\":\n return {\n value: details.value as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n case \"SPLIT\":\n return { value: details.value as T, reason: StandardResolutionReasons.SPLIT };\n case \"DEFAULT\":\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n case \"ERROR\":\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n };\n }\n}\n\nfunction toOFErrorCode(code: EvaluationDetails<unknown>[\"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/server-sdk\";\nimport type { Contexts, ContextValue } from \"@quonfig/node\";\n\n/**\n * Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the first dot: \"user.email\" -> namespace \"user\", key \"email\"\n * - Keys without a dot go to the default (empty-string) namespace: \"country\" -> { \"\": { country: ... } }\n * - Multi-dot keys split on first dot only: \"user.ip.address\" -> { user: { \"ip.address\": ... } }\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, ContextValue>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n // Cast to ContextValue -- OpenFeature allows arbitrary nesting but Quonfig\n // contexts accept primitives and string arrays. Callers should pass only\n // primitive or string[] values for keys they want evaluated.\n const ctxValue = value as ContextValue;\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] = ctxValue;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = ctxValue;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = ctxValue;\n }\n }\n\n return result;\n}\n","import { ErrorCode } from \"@openfeature/server-sdk\";\n\n/**\n * Maps a native SDK error to an OpenFeature ErrorCode.\n *\n * The native SDK throws Error instances with message strings. We map by inspecting\n * the lowercased message.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (\n msg.includes(\"not found\") ||\n msg.includes(\"flag not found\") ||\n msg.includes(\"no value found\") ||\n msg.includes(\"value found for key\")\n ) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (msg.includes(\"not initialized\") || msg.includes(\"provider not ready\")) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["dotIdx","ErrorCode"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quonfig/openfeature-node",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "OpenFeature provider for Quonfig -- Node.js",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -19,16 +19,20 @@
19
19
  "test": "vitest run --exclude test/conformance/**",
20
20
  "test:conformance": "vitest run test/conformance",
21
21
  "lint": "tsc --noEmit",
22
- "prepublishOnly": "npm run build"
22
+ "prepublishOnly": "npm run build",
23
+ "prettier": "prettier . -l",
24
+ "prettier:fix": "prettier --write .",
25
+ "version": "prettier --write CHANGELOG.md README.md && git add CHANGELOG.md README.md"
23
26
  },
24
27
  "peerDependencies": {
25
- "@quonfig/node": ">=0.0.21",
28
+ "@quonfig/node": ">=0.0.26",
26
29
  "@openfeature/server-sdk": ">=1.0.0"
27
30
  },
28
31
  "devDependencies": {
29
- "@quonfig/node": "^0.0.18",
32
+ "@quonfig/node": "^0.0.26",
30
33
  "@openfeature/server-sdk": "^1.7.0",
31
34
  "@types/node": "^20.11.0",
35
+ "prettier": "^3.0.0",
32
36
  "tsup": "^8.0.0",
33
37
  "typescript": "^5.3.0",
34
38
  "vitest": "^1.0.0"