@quonfig/openfeature-node 0.0.2 → 0.0.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/dist/index.cjs +65 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +63 -78
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
29
|
// src/provider.ts
|
|
30
|
-
var
|
|
30
|
+
var import_server_sdk = require("@openfeature/server-sdk");
|
|
31
31
|
var import_node = require("@quonfig/node");
|
|
32
32
|
|
|
33
33
|
// src/context.ts
|
|
@@ -58,33 +58,17 @@ function mapContext(ofContext, targetingKeyMapping = "user.id") {
|
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// src/errors.ts
|
|
62
|
-
var import_server_sdk = require("@openfeature/server-sdk");
|
|
63
|
-
function toErrorCode(err) {
|
|
64
|
-
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
65
|
-
if (msg.includes("not found") || msg.includes("flag not found") || msg.includes("no value found") || msg.includes("value found for key")) {
|
|
66
|
-
return import_server_sdk.ErrorCode.FLAG_NOT_FOUND;
|
|
67
|
-
}
|
|
68
|
-
if (msg.includes("type mismatch")) {
|
|
69
|
-
return import_server_sdk.ErrorCode.TYPE_MISMATCH;
|
|
70
|
-
}
|
|
71
|
-
if (msg.includes("not initialized") || msg.includes("provider not ready")) {
|
|
72
|
-
return import_server_sdk.ErrorCode.PROVIDER_NOT_READY;
|
|
73
|
-
}
|
|
74
|
-
return import_server_sdk.ErrorCode.GENERAL;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
61
|
// src/provider.ts
|
|
78
62
|
var QuonfigProvider = class {
|
|
79
63
|
constructor(options) {
|
|
80
64
|
this.metadata = { name: "quonfig" };
|
|
81
|
-
this.events = new
|
|
65
|
+
this.events = new import_server_sdk.OpenFeatureEventEmitter();
|
|
82
66
|
this.hooks = [];
|
|
83
67
|
this.targetingKeyMapping = options.targetingKeyMapping ?? "user.id";
|
|
84
68
|
this.client = new import_node.Quonfig({
|
|
85
69
|
...options,
|
|
86
70
|
onConfigUpdate: () => {
|
|
87
|
-
this.events.emit(
|
|
71
|
+
this.events.emit(import_server_sdk.ProviderEvents.ConfigurationChanged, { flagsChanged: [] });
|
|
88
72
|
}
|
|
89
73
|
});
|
|
90
74
|
}
|
|
@@ -96,78 +80,30 @@ var QuonfigProvider = class {
|
|
|
96
80
|
}
|
|
97
81
|
async resolveBooleanEvaluation(flagKey, defaultValue, context, _logger) {
|
|
98
82
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (value === void 0) {
|
|
102
|
-
return { value: defaultValue, reason: import_server_sdk2.StandardResolutionReasons.DEFAULT };
|
|
103
|
-
}
|
|
104
|
-
return { value, reason: import_server_sdk2.StandardResolutionReasons.TARGETING_MATCH };
|
|
105
|
-
} catch (err) {
|
|
106
|
-
return {
|
|
107
|
-
value: defaultValue,
|
|
108
|
-
reason: import_server_sdk2.StandardResolutionReasons.ERROR,
|
|
109
|
-
errorCode: toErrorCode(err),
|
|
110
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
111
|
-
};
|
|
112
|
-
}
|
|
83
|
+
const details = this.client.getBoolDetails(flagKey, mappedCtx);
|
|
84
|
+
return toResolutionDetails(details, defaultValue);
|
|
113
85
|
}
|
|
114
86
|
async resolveStringEvaluation(flagKey, defaultValue, context, _logger) {
|
|
115
87
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (value === void 0) {
|
|
119
|
-
return { value: defaultValue, reason: import_server_sdk2.StandardResolutionReasons.DEFAULT };
|
|
120
|
-
}
|
|
121
|
-
return { value, reason: import_server_sdk2.StandardResolutionReasons.TARGETING_MATCH };
|
|
122
|
-
} catch (err) {
|
|
123
|
-
return {
|
|
124
|
-
value: defaultValue,
|
|
125
|
-
reason: import_server_sdk2.StandardResolutionReasons.ERROR,
|
|
126
|
-
errorCode: toErrorCode(err),
|
|
127
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
128
|
-
};
|
|
129
|
-
}
|
|
88
|
+
const details = this.client.getStringDetails(flagKey, mappedCtx);
|
|
89
|
+
return toResolutionDetails(details, defaultValue);
|
|
130
90
|
}
|
|
131
91
|
async resolveNumberEvaluation(flagKey, defaultValue, context, _logger) {
|
|
132
92
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (value === void 0) {
|
|
136
|
-
return { value: defaultValue, reason: import_server_sdk2.StandardResolutionReasons.DEFAULT };
|
|
137
|
-
}
|
|
138
|
-
return { value, reason: import_server_sdk2.StandardResolutionReasons.TARGETING_MATCH };
|
|
139
|
-
} catch (err) {
|
|
140
|
-
return {
|
|
141
|
-
value: defaultValue,
|
|
142
|
-
reason: import_server_sdk2.StandardResolutionReasons.ERROR,
|
|
143
|
-
errorCode: toErrorCode(err),
|
|
144
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
145
|
-
};
|
|
146
|
-
}
|
|
93
|
+
const details = this.client.getNumberDetails(flagKey, mappedCtx);
|
|
94
|
+
return toResolutionDetails(details, defaultValue);
|
|
147
95
|
}
|
|
148
96
|
async resolveObjectEvaluation(flagKey, defaultValue, context, _logger) {
|
|
149
97
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const jsonVal = this.client.getJSON(flagKey, mappedCtx);
|
|
159
|
-
if (jsonVal !== void 0) {
|
|
160
|
-
return { value: jsonVal, reason: import_server_sdk2.StandardResolutionReasons.TARGETING_MATCH };
|
|
161
|
-
}
|
|
162
|
-
return { value: defaultValue, reason: import_server_sdk2.StandardResolutionReasons.DEFAULT };
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return {
|
|
165
|
-
value: defaultValue,
|
|
166
|
-
reason: import_server_sdk2.StandardResolutionReasons.ERROR,
|
|
167
|
-
errorCode: toErrorCode(err),
|
|
168
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
169
|
-
};
|
|
98
|
+
const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);
|
|
99
|
+
if (listDetails.reason === "STATIC" || listDetails.reason === "TARGETING_MATCH" || listDetails.reason === "SPLIT") {
|
|
100
|
+
return toResolutionDetails(
|
|
101
|
+
listDetails,
|
|
102
|
+
defaultValue
|
|
103
|
+
);
|
|
170
104
|
}
|
|
105
|
+
const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);
|
|
106
|
+
return toResolutionDetails(jsonDetails, defaultValue);
|
|
171
107
|
}
|
|
172
108
|
/**
|
|
173
109
|
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
@@ -177,6 +113,54 @@ var QuonfigProvider = class {
|
|
|
177
113
|
return this.client;
|
|
178
114
|
}
|
|
179
115
|
};
|
|
116
|
+
function toResolutionDetails(details, defaultValue) {
|
|
117
|
+
switch (details.reason) {
|
|
118
|
+
case "STATIC":
|
|
119
|
+
return { value: details.value, reason: import_server_sdk.StandardResolutionReasons.STATIC };
|
|
120
|
+
case "TARGETING_MATCH":
|
|
121
|
+
return {
|
|
122
|
+
value: details.value,
|
|
123
|
+
reason: import_server_sdk.StandardResolutionReasons.TARGETING_MATCH
|
|
124
|
+
};
|
|
125
|
+
case "SPLIT":
|
|
126
|
+
return { value: details.value, reason: import_server_sdk.StandardResolutionReasons.SPLIT };
|
|
127
|
+
case "DEFAULT":
|
|
128
|
+
return { value: defaultValue, reason: import_server_sdk.StandardResolutionReasons.DEFAULT };
|
|
129
|
+
case "ERROR":
|
|
130
|
+
return {
|
|
131
|
+
value: defaultValue,
|
|
132
|
+
reason: import_server_sdk.StandardResolutionReasons.ERROR,
|
|
133
|
+
errorCode: toOFErrorCode(details.errorCode)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function toOFErrorCode(code) {
|
|
138
|
+
switch (code) {
|
|
139
|
+
case "FLAG_NOT_FOUND":
|
|
140
|
+
return import_server_sdk.ErrorCode.FLAG_NOT_FOUND;
|
|
141
|
+
case "TYPE_MISMATCH":
|
|
142
|
+
return import_server_sdk.ErrorCode.TYPE_MISMATCH;
|
|
143
|
+
case "GENERAL":
|
|
144
|
+
default:
|
|
145
|
+
return import_server_sdk.ErrorCode.GENERAL;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/errors.ts
|
|
150
|
+
var import_server_sdk2 = require("@openfeature/server-sdk");
|
|
151
|
+
function toErrorCode(err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
153
|
+
if (msg.includes("not found") || msg.includes("flag not found") || msg.includes("no value found") || msg.includes("value found for key")) {
|
|
154
|
+
return import_server_sdk2.ErrorCode.FLAG_NOT_FOUND;
|
|
155
|
+
}
|
|
156
|
+
if (msg.includes("type mismatch")) {
|
|
157
|
+
return import_server_sdk2.ErrorCode.TYPE_MISMATCH;
|
|
158
|
+
}
|
|
159
|
+
if (msg.includes("not initialized") || msg.includes("provider not ready")) {
|
|
160
|
+
return import_server_sdk2.ErrorCode.PROVIDER_NOT_READY;
|
|
161
|
+
}
|
|
162
|
+
return import_server_sdk2.ErrorCode.GENERAL;
|
|
163
|
+
}
|
|
180
164
|
// Annotate the CommonJS export names for ESM import in node:
|
|
181
165
|
0 && (module.exports = {
|
|
182
166
|
QuonfigProvider,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\nimport { toErrorCode } from \"./errors.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 // onConfigUpdate is not yet in the published @quonfig/node types; cast until sdk-node ships it\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n } as ConstructorParameters<typeof Quonfig>[0]);\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n 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 try {\n const value = this.client.getBool(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 try {\n const value = this.client.getString(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 try {\n const value = this.client.getNumber(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 {\n // Try string_list first (returns string[])\n const listVal = this.client.getStringList(flagKey, mappedCtx);\n if (listVal !== undefined) {\n return {\n value: listVal as unknown as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n }\n // Fall back to JSON\n const jsonVal = this.client.getJSON(flagKey, mappedCtx);\n if (jsonVal !== undefined) {\n return { value: jsonVal as T, reason: StandardResolutionReasons.TARGETING_MATCH };\n }\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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","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":["import_server_sdk","dotIdx"]}
|
|
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 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"]}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/provider.ts
|
|
2
2
|
import {
|
|
3
|
+
ErrorCode,
|
|
3
4
|
OpenFeatureEventEmitter,
|
|
4
5
|
ProviderEvents,
|
|
5
6
|
StandardResolutionReasons
|
|
@@ -34,22 +35,6 @@ function mapContext(ofContext, targetingKeyMapping = "user.id") {
|
|
|
34
35
|
return result;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// src/errors.ts
|
|
38
|
-
import { ErrorCode } from "@openfeature/server-sdk";
|
|
39
|
-
function toErrorCode(err) {
|
|
40
|
-
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
41
|
-
if (msg.includes("not found") || msg.includes("flag not found") || msg.includes("no value found") || msg.includes("value found for key")) {
|
|
42
|
-
return ErrorCode.FLAG_NOT_FOUND;
|
|
43
|
-
}
|
|
44
|
-
if (msg.includes("type mismatch")) {
|
|
45
|
-
return ErrorCode.TYPE_MISMATCH;
|
|
46
|
-
}
|
|
47
|
-
if (msg.includes("not initialized") || msg.includes("provider not ready")) {
|
|
48
|
-
return ErrorCode.PROVIDER_NOT_READY;
|
|
49
|
-
}
|
|
50
|
-
return ErrorCode.GENERAL;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
38
|
// src/provider.ts
|
|
54
39
|
var QuonfigProvider = class {
|
|
55
40
|
constructor(options) {
|
|
@@ -72,78 +57,30 @@ var QuonfigProvider = class {
|
|
|
72
57
|
}
|
|
73
58
|
async resolveBooleanEvaluation(flagKey, defaultValue, context, _logger) {
|
|
74
59
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (value === void 0) {
|
|
78
|
-
return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };
|
|
79
|
-
}
|
|
80
|
-
return { value, reason: StandardResolutionReasons.TARGETING_MATCH };
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return {
|
|
83
|
-
value: defaultValue,
|
|
84
|
-
reason: StandardResolutionReasons.ERROR,
|
|
85
|
-
errorCode: toErrorCode(err),
|
|
86
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
60
|
+
const details = this.client.getBoolDetails(flagKey, mappedCtx);
|
|
61
|
+
return toResolutionDetails(details, defaultValue);
|
|
89
62
|
}
|
|
90
63
|
async resolveStringEvaluation(flagKey, defaultValue, context, _logger) {
|
|
91
64
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (value === void 0) {
|
|
95
|
-
return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };
|
|
96
|
-
}
|
|
97
|
-
return { value, reason: StandardResolutionReasons.TARGETING_MATCH };
|
|
98
|
-
} catch (err) {
|
|
99
|
-
return {
|
|
100
|
-
value: defaultValue,
|
|
101
|
-
reason: StandardResolutionReasons.ERROR,
|
|
102
|
-
errorCode: toErrorCode(err),
|
|
103
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
104
|
-
};
|
|
105
|
-
}
|
|
65
|
+
const details = this.client.getStringDetails(flagKey, mappedCtx);
|
|
66
|
+
return toResolutionDetails(details, defaultValue);
|
|
106
67
|
}
|
|
107
68
|
async resolveNumberEvaluation(flagKey, defaultValue, context, _logger) {
|
|
108
69
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (value === void 0) {
|
|
112
|
-
return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };
|
|
113
|
-
}
|
|
114
|
-
return { value, reason: StandardResolutionReasons.TARGETING_MATCH };
|
|
115
|
-
} catch (err) {
|
|
116
|
-
return {
|
|
117
|
-
value: defaultValue,
|
|
118
|
-
reason: StandardResolutionReasons.ERROR,
|
|
119
|
-
errorCode: toErrorCode(err),
|
|
120
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
70
|
+
const details = this.client.getNumberDetails(flagKey, mappedCtx);
|
|
71
|
+
return toResolutionDetails(details, defaultValue);
|
|
123
72
|
}
|
|
124
73
|
async resolveObjectEvaluation(flagKey, defaultValue, context, _logger) {
|
|
125
74
|
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const jsonVal = this.client.getJSON(flagKey, mappedCtx);
|
|
135
|
-
if (jsonVal !== void 0) {
|
|
136
|
-
return { value: jsonVal, reason: StandardResolutionReasons.TARGETING_MATCH };
|
|
137
|
-
}
|
|
138
|
-
return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };
|
|
139
|
-
} catch (err) {
|
|
140
|
-
return {
|
|
141
|
-
value: defaultValue,
|
|
142
|
-
reason: StandardResolutionReasons.ERROR,
|
|
143
|
-
errorCode: toErrorCode(err),
|
|
144
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
145
|
-
};
|
|
75
|
+
const listDetails = this.client.getStringListDetails(flagKey, mappedCtx);
|
|
76
|
+
if (listDetails.reason === "STATIC" || listDetails.reason === "TARGETING_MATCH" || listDetails.reason === "SPLIT") {
|
|
77
|
+
return toResolutionDetails(
|
|
78
|
+
listDetails,
|
|
79
|
+
defaultValue
|
|
80
|
+
);
|
|
146
81
|
}
|
|
82
|
+
const jsonDetails = this.client.getJSONDetails(flagKey, mappedCtx);
|
|
83
|
+
return toResolutionDetails(jsonDetails, defaultValue);
|
|
147
84
|
}
|
|
148
85
|
/**
|
|
149
86
|
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
@@ -153,6 +90,54 @@ var QuonfigProvider = class {
|
|
|
153
90
|
return this.client;
|
|
154
91
|
}
|
|
155
92
|
};
|
|
93
|
+
function toResolutionDetails(details, defaultValue) {
|
|
94
|
+
switch (details.reason) {
|
|
95
|
+
case "STATIC":
|
|
96
|
+
return { value: details.value, reason: StandardResolutionReasons.STATIC };
|
|
97
|
+
case "TARGETING_MATCH":
|
|
98
|
+
return {
|
|
99
|
+
value: details.value,
|
|
100
|
+
reason: StandardResolutionReasons.TARGETING_MATCH
|
|
101
|
+
};
|
|
102
|
+
case "SPLIT":
|
|
103
|
+
return { value: details.value, reason: StandardResolutionReasons.SPLIT };
|
|
104
|
+
case "DEFAULT":
|
|
105
|
+
return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };
|
|
106
|
+
case "ERROR":
|
|
107
|
+
return {
|
|
108
|
+
value: defaultValue,
|
|
109
|
+
reason: StandardResolutionReasons.ERROR,
|
|
110
|
+
errorCode: toOFErrorCode(details.errorCode)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function toOFErrorCode(code) {
|
|
115
|
+
switch (code) {
|
|
116
|
+
case "FLAG_NOT_FOUND":
|
|
117
|
+
return ErrorCode.FLAG_NOT_FOUND;
|
|
118
|
+
case "TYPE_MISMATCH":
|
|
119
|
+
return ErrorCode.TYPE_MISMATCH;
|
|
120
|
+
case "GENERAL":
|
|
121
|
+
default:
|
|
122
|
+
return ErrorCode.GENERAL;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/errors.ts
|
|
127
|
+
import { ErrorCode as ErrorCode2 } from "@openfeature/server-sdk";
|
|
128
|
+
function toErrorCode(err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
130
|
+
if (msg.includes("not found") || msg.includes("flag not found") || msg.includes("no value found") || msg.includes("value found for key")) {
|
|
131
|
+
return ErrorCode2.FLAG_NOT_FOUND;
|
|
132
|
+
}
|
|
133
|
+
if (msg.includes("type mismatch")) {
|
|
134
|
+
return ErrorCode2.TYPE_MISMATCH;
|
|
135
|
+
}
|
|
136
|
+
if (msg.includes("not initialized") || msg.includes("provider not ready")) {
|
|
137
|
+
return ErrorCode2.PROVIDER_NOT_READY;
|
|
138
|
+
}
|
|
139
|
+
return ErrorCode2.GENERAL;
|
|
140
|
+
}
|
|
156
141
|
export {
|
|
157
142
|
QuonfigProvider,
|
|
158
143
|
mapContext,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n EvaluationContext,\n JsonValue,\n Logger,\n OpenFeatureEventEmitter,\n Provider,\n ProviderEvents,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/server-sdk\";\nimport { Quonfig, QuonfigOptions } from \"@quonfig/node\";\nimport { mapContext } from \"./context.js\";\nimport { toErrorCode } from \"./errors.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 // onConfigUpdate is not yet in the published @quonfig/node types; cast until sdk-node ships it\n this.client = new Quonfig({\n ...options,\n onConfigUpdate: () => {\n this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });\n },\n } as ConstructorParameters<typeof Quonfig>[0]);\n }\n\n async initialize(_context?: EvaluationContext): Promise<void> {\n await this.client.init();\n }\n\n async shutdown(): Promise<void> {\n 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 try {\n const value = this.client.getBool(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 try {\n const value = this.client.getString(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 try {\n const value = this.client.getNumber(flagKey, mappedCtx);\n if (value === undefined) {\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n }\n return { value, reason: StandardResolutionReasons.TARGETING_MATCH };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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 {\n // Try string_list first (returns string[])\n const listVal = this.client.getStringList(flagKey, mappedCtx);\n if (listVal !== undefined) {\n return {\n value: listVal as unknown as T,\n reason: StandardResolutionReasons.TARGETING_MATCH,\n };\n }\n // Fall back to JSON\n const jsonVal = this.client.getJSON(flagKey, mappedCtx);\n if (jsonVal !== undefined) {\n return { value: jsonVal as T, reason: StandardResolutionReasons.TARGETING_MATCH };\n }\n return { value: defaultValue, reason: StandardResolutionReasons.DEFAULT };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n errorMessage: err instanceof Error ? err.message : String(err),\n };\n }\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","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"]}
|
|
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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quonfig/openfeature-node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "OpenFeature provider for Quonfig -- Node.js",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"prepublishOnly": "npm run build"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@quonfig/node": ">=0.0.
|
|
25
|
+
"@quonfig/node": ">=0.0.18",
|
|
26
26
|
"@openfeature/server-sdk": ">=1.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@quonfig/node": "^0.0.
|
|
29
|
+
"@quonfig/node": "^0.0.18",
|
|
30
30
|
"@openfeature/server-sdk": "^1.7.0",
|
|
31
31
|
"@types/node": "^20.11.0",
|
|
32
32
|
"tsup": "^8.0.0",
|