@quonfig/openfeature-node 0.0.1
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 +119 -0
- package/dist/index.cjs +186 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @quonfig/openfeature-node
|
|
2
|
+
|
|
3
|
+
OpenFeature provider for [Quonfig](https://quonfig.com) -- Node.js server-side SDK.
|
|
4
|
+
|
|
5
|
+
This package wraps the `@quonfig/node` native SDK and implements the
|
|
6
|
+
[OpenFeature](https://openfeature.dev) server-side `Provider` interface.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @quonfig/openfeature-node @quonfig/node @openfeature/server-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { QuonfigProvider } from "@quonfig/openfeature-node";
|
|
18
|
+
import { OpenFeature } from "@openfeature/server-sdk";
|
|
19
|
+
|
|
20
|
+
const provider = new QuonfigProvider({
|
|
21
|
+
sdkKey: "qf_sk_production_...",
|
|
22
|
+
// targetingKeyMapping: "user.id", // default
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await OpenFeature.setProviderAndWait(provider);
|
|
26
|
+
|
|
27
|
+
const client = OpenFeature.getClient();
|
|
28
|
+
|
|
29
|
+
// Boolean flag
|
|
30
|
+
const enabled = await client.getBooleanValue("my-feature", false);
|
|
31
|
+
|
|
32
|
+
// String config
|
|
33
|
+
const welcomeMsg = await client.getStringValue("welcome-message", "Hello!");
|
|
34
|
+
|
|
35
|
+
// Number config
|
|
36
|
+
const timeout = await client.getNumberValue("request-timeout-ms", 5000);
|
|
37
|
+
|
|
38
|
+
// Object config (JSON or string_list)
|
|
39
|
+
const allowedPlans = await client.getObjectValue("allowed-plans", []);
|
|
40
|
+
|
|
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
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Context mapping
|
|
54
|
+
|
|
55
|
+
OpenFeature context is flat; Quonfig context is nested by namespace. This provider
|
|
56
|
+
maps between them using dot-notation:
|
|
57
|
+
|
|
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) |
|
|
65
|
+
|
|
66
|
+
### Customizing targetingKey mapping
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const provider = new QuonfigProvider({
|
|
70
|
+
sdkKey: "qf_sk_...",
|
|
71
|
+
targetingKeyMapping: "account.id", // maps targetingKey to { account: { id: ... } }
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Accessing native SDK features
|
|
76
|
+
|
|
77
|
+
The `getClient()` escape hatch returns the underlying `@quonfig/node` client for
|
|
78
|
+
features not available in OpenFeature:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const native = provider.getClient();
|
|
82
|
+
|
|
83
|
+
// Log level integration
|
|
84
|
+
const shouldLog = native.shouldLog({
|
|
85
|
+
loggerName: "auth",
|
|
86
|
+
desiredLevel: "DEBUG",
|
|
87
|
+
contexts: { user: { id: "user-123" } },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// List all config keys
|
|
91
|
+
const keys = native.keys();
|
|
92
|
+
|
|
93
|
+
// Access raw config
|
|
94
|
+
const rawConfig = native.rawConfig("my-flag");
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## What you lose vs. the native SDK
|
|
98
|
+
|
|
99
|
+
OpenFeature is designed for feature flags, not general configuration. Some Quonfig
|
|
100
|
+
features require the native `@quonfig/node` SDK:
|
|
101
|
+
|
|
102
|
+
1. **Log levels** -- `shouldLog()` and `logger()` are native-only.
|
|
103
|
+
2. **`string_list` configs** -- must be accessed via `getObjectValue()` and cast to `string[]`.
|
|
104
|
+
3. **`duration` configs** -- return the raw millisecond number via `getNumberValue()`.
|
|
105
|
+
4. **`bytes` configs** -- not accessible via OpenFeature (no binary type in OF).
|
|
106
|
+
5. **`keys()` and `rawConfig()`** -- native-only via `getClient()`.
|
|
107
|
+
6. **Context keys use dot-notation** -- `"user.email"`, not nested objects.
|
|
108
|
+
7. **`targetingKey` maps to `user.id` by default** -- configure `targetingKeyMapping` if different.
|
|
109
|
+
|
|
110
|
+
## Configuration changed events
|
|
111
|
+
|
|
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:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, () => {
|
|
117
|
+
console.log("Configs updated!");
|
|
118
|
+
});
|
|
119
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
QuonfigProvider: () => QuonfigProvider,
|
|
24
|
+
mapContext: () => mapContext,
|
|
25
|
+
toErrorCode: () => toErrorCode
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/provider.ts
|
|
30
|
+
var import_server_sdk2 = require("@openfeature/server-sdk");
|
|
31
|
+
var import_node = require("@quonfig/node");
|
|
32
|
+
|
|
33
|
+
// src/context.ts
|
|
34
|
+
function mapContext(ofContext, targetingKeyMapping = "user.id") {
|
|
35
|
+
const result = {};
|
|
36
|
+
for (const [key, value] of Object.entries(ofContext)) {
|
|
37
|
+
if (value === void 0) continue;
|
|
38
|
+
const ctxValue = value;
|
|
39
|
+
if (key === "targetingKey") {
|
|
40
|
+
const dotIdx2 = targetingKeyMapping.indexOf(".");
|
|
41
|
+
const ns = dotIdx2 === -1 ? "" : targetingKeyMapping.slice(0, dotIdx2);
|
|
42
|
+
const prop = dotIdx2 === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx2 + 1);
|
|
43
|
+
result[ns] ??= {};
|
|
44
|
+
result[ns][prop] = ctxValue;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const dotIdx = key.indexOf(".");
|
|
48
|
+
if (dotIdx === -1) {
|
|
49
|
+
result[""] ??= {};
|
|
50
|
+
result[""][key] = ctxValue;
|
|
51
|
+
} else {
|
|
52
|
+
const ns = key.slice(0, dotIdx);
|
|
53
|
+
const prop = key.slice(dotIdx + 1);
|
|
54
|
+
result[ns] ??= {};
|
|
55
|
+
result[ns][prop] = ctxValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
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
|
+
// src/provider.ts
|
|
78
|
+
var QuonfigProvider = class {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.metadata = { name: "quonfig" };
|
|
81
|
+
this.events = new import_server_sdk2.OpenFeatureEventEmitter();
|
|
82
|
+
this.hooks = [];
|
|
83
|
+
this.targetingKeyMapping = options.targetingKeyMapping ?? "user.id";
|
|
84
|
+
this.client = new import_node.Quonfig({
|
|
85
|
+
...options,
|
|
86
|
+
onConfigUpdate: () => {
|
|
87
|
+
this.events.emit(import_server_sdk2.ProviderEvents.ConfigurationChanged, { flagsChanged: [] });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async initialize(_context) {
|
|
92
|
+
await this.client.init();
|
|
93
|
+
}
|
|
94
|
+
async shutdown() {
|
|
95
|
+
this.client.close();
|
|
96
|
+
}
|
|
97
|
+
async resolveBooleanEvaluation(flagKey, defaultValue, context, _logger) {
|
|
98
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
99
|
+
try {
|
|
100
|
+
const value = this.client.getBool(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async resolveStringEvaluation(flagKey, defaultValue, context, _logger) {
|
|
115
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
116
|
+
try {
|
|
117
|
+
const value = this.client.getString(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async resolveNumberEvaluation(flagKey, defaultValue, context, _logger) {
|
|
132
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
133
|
+
try {
|
|
134
|
+
const value = this.client.getNumber(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async resolveObjectEvaluation(flagKey, defaultValue, context, _logger) {
|
|
149
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
150
|
+
try {
|
|
151
|
+
const listVal = this.client.getStringList(flagKey, mappedCtx);
|
|
152
|
+
if (listVal !== void 0) {
|
|
153
|
+
return {
|
|
154
|
+
value: listVal,
|
|
155
|
+
reason: import_server_sdk2.StandardResolutionReasons.TARGETING_MATCH
|
|
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
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
174
|
+
* (shouldLog, keys, rawConfig, etc.)
|
|
175
|
+
*/
|
|
176
|
+
getClient() {
|
|
177
|
+
return this.client;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
181
|
+
0 && (module.exports = {
|
|
182
|
+
QuonfigProvider,
|
|
183
|
+
mapContext,
|
|
184
|
+
toErrorCode
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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 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 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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Provider, OpenFeatureEventEmitter, EvaluationContext, Logger, ResolutionDetails, JsonValue, ErrorCode } from '@openfeature/server-sdk';
|
|
2
|
+
import { QuonfigOptions, Quonfig, Contexts } from '@quonfig/node';
|
|
3
|
+
|
|
4
|
+
interface QuonfigProviderOptions extends Omit<QuonfigOptions, "onConfigUpdate"> {
|
|
5
|
+
/**
|
|
6
|
+
* Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.
|
|
7
|
+
* Defaults to "user.id".
|
|
8
|
+
*/
|
|
9
|
+
targetingKeyMapping?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* QuonfigProvider wraps the @quonfig/node native SDK and implements the
|
|
13
|
+
* OpenFeature server-side Provider interface.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { QuonfigProvider } from "@quonfig/openfeature-node";
|
|
18
|
+
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
19
|
+
*
|
|
20
|
+
* const provider = new QuonfigProvider({ sdkKey: "qf_sk_..." });
|
|
21
|
+
* await OpenFeature.setProviderAndWait(provider);
|
|
22
|
+
* const client = OpenFeature.getClient();
|
|
23
|
+
* const enabled = await client.getBooleanValue("my-flag", false);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare class QuonfigProvider implements Provider {
|
|
27
|
+
readonly metadata: {
|
|
28
|
+
readonly name: "quonfig";
|
|
29
|
+
};
|
|
30
|
+
readonly events: OpenFeatureEventEmitter;
|
|
31
|
+
readonly hooks: never[];
|
|
32
|
+
private readonly client;
|
|
33
|
+
private readonly targetingKeyMapping;
|
|
34
|
+
constructor(options: QuonfigProviderOptions);
|
|
35
|
+
initialize(_context?: EvaluationContext): Promise<void>;
|
|
36
|
+
shutdown(): Promise<void>;
|
|
37
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<boolean>>;
|
|
38
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<string>>;
|
|
39
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<number>>;
|
|
40
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<T>>;
|
|
41
|
+
/**
|
|
42
|
+
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
43
|
+
* (shouldLog, keys, rawConfig, etc.)
|
|
44
|
+
*/
|
|
45
|
+
getClient(): Quonfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.
|
|
50
|
+
*
|
|
51
|
+
* Rules:
|
|
52
|
+
* - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: "user.id")
|
|
53
|
+
* - Keys with a dot are split on the first dot: "user.email" -> namespace "user", key "email"
|
|
54
|
+
* - Keys without a dot go to the default (empty-string) namespace: "country" -> { "": { country: ... } }
|
|
55
|
+
* - Multi-dot keys split on first dot only: "user.ip.address" -> { user: { "ip.address": ... } }
|
|
56
|
+
*/
|
|
57
|
+
declare function mapContext(ofContext: EvaluationContext, targetingKeyMapping?: string): Contexts;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Maps a native SDK error to an OpenFeature ErrorCode.
|
|
61
|
+
*
|
|
62
|
+
* The native SDK throws Error instances with message strings. We map by inspecting
|
|
63
|
+
* the lowercased message.
|
|
64
|
+
*/
|
|
65
|
+
declare function toErrorCode(err: unknown): ErrorCode;
|
|
66
|
+
|
|
67
|
+
export { QuonfigProvider, type QuonfigProviderOptions, mapContext, toErrorCode };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Provider, OpenFeatureEventEmitter, EvaluationContext, Logger, ResolutionDetails, JsonValue, ErrorCode } from '@openfeature/server-sdk';
|
|
2
|
+
import { QuonfigOptions, Quonfig, Contexts } from '@quonfig/node';
|
|
3
|
+
|
|
4
|
+
interface QuonfigProviderOptions extends Omit<QuonfigOptions, "onConfigUpdate"> {
|
|
5
|
+
/**
|
|
6
|
+
* Dot-notation path to map OpenFeature's `targetingKey` into Quonfig's nested context.
|
|
7
|
+
* Defaults to "user.id".
|
|
8
|
+
*/
|
|
9
|
+
targetingKeyMapping?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* QuonfigProvider wraps the @quonfig/node native SDK and implements the
|
|
13
|
+
* OpenFeature server-side Provider interface.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { QuonfigProvider } from "@quonfig/openfeature-node";
|
|
18
|
+
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
19
|
+
*
|
|
20
|
+
* const provider = new QuonfigProvider({ sdkKey: "qf_sk_..." });
|
|
21
|
+
* await OpenFeature.setProviderAndWait(provider);
|
|
22
|
+
* const client = OpenFeature.getClient();
|
|
23
|
+
* const enabled = await client.getBooleanValue("my-flag", false);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare class QuonfigProvider implements Provider {
|
|
27
|
+
readonly metadata: {
|
|
28
|
+
readonly name: "quonfig";
|
|
29
|
+
};
|
|
30
|
+
readonly events: OpenFeatureEventEmitter;
|
|
31
|
+
readonly hooks: never[];
|
|
32
|
+
private readonly client;
|
|
33
|
+
private readonly targetingKeyMapping;
|
|
34
|
+
constructor(options: QuonfigProviderOptions);
|
|
35
|
+
initialize(_context?: EvaluationContext): Promise<void>;
|
|
36
|
+
shutdown(): Promise<void>;
|
|
37
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<boolean>>;
|
|
38
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<string>>;
|
|
39
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<number>>;
|
|
40
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<T>>;
|
|
41
|
+
/**
|
|
42
|
+
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
43
|
+
* (shouldLog, keys, rawConfig, etc.)
|
|
44
|
+
*/
|
|
45
|
+
getClient(): Quonfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maps an OpenFeature flat EvaluationContext to Quonfig's nested Contexts format.
|
|
50
|
+
*
|
|
51
|
+
* Rules:
|
|
52
|
+
* - `targetingKey` maps to the namespace+property specified by `targetingKeyMapping` (default: "user.id")
|
|
53
|
+
* - Keys with a dot are split on the first dot: "user.email" -> namespace "user", key "email"
|
|
54
|
+
* - Keys without a dot go to the default (empty-string) namespace: "country" -> { "": { country: ... } }
|
|
55
|
+
* - Multi-dot keys split on first dot only: "user.ip.address" -> { user: { "ip.address": ... } }
|
|
56
|
+
*/
|
|
57
|
+
declare function mapContext(ofContext: EvaluationContext, targetingKeyMapping?: string): Contexts;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Maps a native SDK error to an OpenFeature ErrorCode.
|
|
61
|
+
*
|
|
62
|
+
* The native SDK throws Error instances with message strings. We map by inspecting
|
|
63
|
+
* the lowercased message.
|
|
64
|
+
*/
|
|
65
|
+
declare function toErrorCode(err: unknown): ErrorCode;
|
|
66
|
+
|
|
67
|
+
export { QuonfigProvider, type QuonfigProviderOptions, mapContext, toErrorCode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/provider.ts
|
|
2
|
+
import {
|
|
3
|
+
OpenFeatureEventEmitter,
|
|
4
|
+
ProviderEvents,
|
|
5
|
+
StandardResolutionReasons
|
|
6
|
+
} from "@openfeature/server-sdk";
|
|
7
|
+
import { Quonfig } from "@quonfig/node";
|
|
8
|
+
|
|
9
|
+
// src/context.ts
|
|
10
|
+
function mapContext(ofContext, targetingKeyMapping = "user.id") {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [key, value] of Object.entries(ofContext)) {
|
|
13
|
+
if (value === void 0) continue;
|
|
14
|
+
const ctxValue = value;
|
|
15
|
+
if (key === "targetingKey") {
|
|
16
|
+
const dotIdx2 = targetingKeyMapping.indexOf(".");
|
|
17
|
+
const ns = dotIdx2 === -1 ? "" : targetingKeyMapping.slice(0, dotIdx2);
|
|
18
|
+
const prop = dotIdx2 === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx2 + 1);
|
|
19
|
+
result[ns] ??= {};
|
|
20
|
+
result[ns][prop] = ctxValue;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const dotIdx = key.indexOf(".");
|
|
24
|
+
if (dotIdx === -1) {
|
|
25
|
+
result[""] ??= {};
|
|
26
|
+
result[""][key] = ctxValue;
|
|
27
|
+
} else {
|
|
28
|
+
const ns = key.slice(0, dotIdx);
|
|
29
|
+
const prop = key.slice(dotIdx + 1);
|
|
30
|
+
result[ns] ??= {};
|
|
31
|
+
result[ns][prop] = ctxValue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
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
|
+
// src/provider.ts
|
|
54
|
+
var QuonfigProvider = class {
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.metadata = { name: "quonfig" };
|
|
57
|
+
this.events = new OpenFeatureEventEmitter();
|
|
58
|
+
this.hooks = [];
|
|
59
|
+
this.targetingKeyMapping = options.targetingKeyMapping ?? "user.id";
|
|
60
|
+
this.client = new Quonfig({
|
|
61
|
+
...options,
|
|
62
|
+
onConfigUpdate: () => {
|
|
63
|
+
this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [] });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async initialize(_context) {
|
|
68
|
+
await this.client.init();
|
|
69
|
+
}
|
|
70
|
+
async shutdown() {
|
|
71
|
+
this.client.close();
|
|
72
|
+
}
|
|
73
|
+
async resolveBooleanEvaluation(flagKey, defaultValue, context, _logger) {
|
|
74
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
75
|
+
try {
|
|
76
|
+
const value = this.client.getBool(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async resolveStringEvaluation(flagKey, defaultValue, context, _logger) {
|
|
91
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
92
|
+
try {
|
|
93
|
+
const value = this.client.getString(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async resolveNumberEvaluation(flagKey, defaultValue, context, _logger) {
|
|
108
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
109
|
+
try {
|
|
110
|
+
const value = this.client.getNumber(flagKey, mappedCtx);
|
|
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
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async resolveObjectEvaluation(flagKey, defaultValue, context, _logger) {
|
|
125
|
+
const mappedCtx = mapContext(context, this.targetingKeyMapping);
|
|
126
|
+
try {
|
|
127
|
+
const listVal = this.client.getStringList(flagKey, mappedCtx);
|
|
128
|
+
if (listVal !== void 0) {
|
|
129
|
+
return {
|
|
130
|
+
value: listVal,
|
|
131
|
+
reason: StandardResolutionReasons.TARGETING_MATCH
|
|
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
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Escape hatch: access the underlying @quonfig/node client for native-only features
|
|
150
|
+
* (shouldLog, keys, rawConfig, etc.)
|
|
151
|
+
*/
|
|
152
|
+
getClient() {
|
|
153
|
+
return this.client;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export {
|
|
157
|
+
QuonfigProvider,
|
|
158
|
+
mapContext,
|
|
159
|
+
toErrorCode
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 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 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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quonfig/openfeature-node",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenFeature provider for Quonfig -- Node.js",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "rm -rf dist/ && npx tsup",
|
|
19
|
+
"test": "vitest run --exclude test/conformance/**",
|
|
20
|
+
"test:conformance": "vitest run test/conformance",
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@quonfig/node": ">=0.0.6",
|
|
26
|
+
"@openfeature/server-sdk": ">=1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@quonfig/node": "^0.0.6",
|
|
30
|
+
"@openfeature/server-sdk": "^1.7.0",
|
|
31
|
+
"@types/node": "^20.11.0",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.3.0",
|
|
34
|
+
"vitest": "^1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": { "access": "public" },
|
|
37
|
+
"author": "Quonfig",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/quonfig/openfeature-node.git"
|
|
42
|
+
}
|
|
43
|
+
}
|