@devcycle/nodejs-server-sdk
Version:
The DevCycle NodeJS Server SDK used for feature management.
286 lines • 11.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.DevCycleProvider = void 0;
const server_sdk_1 = require("@openfeature/server-sdk");
const index_1 = require("../index");
const DVCKnownPropertyKeyTypes = {
email: 'string',
name: 'string',
language: 'string',
country: 'string',
appVersion: 'string',
appBuild: 'number',
customData: 'object',
privateCustomData: 'object',
};
class DevCycleProvider {
constructor(clientOrKey, options = {}) {
var _a;
this.metadata = {
name: 'devcycle-nodejs-provider',
};
this.runsOn = 'server';
if (typeof clientOrKey === 'string') {
this.devcycleClient = (0, index_1.initializeDevCycle)(clientOrKey, {
...options,
sdkPlatform: 'nodejs-of',
});
}
else {
this.devcycleClient = clientOrKey;
}
this.logger =
(_a = options.logger) !== null && _a !== void 0 ? _a : (0, index_1.dvcDefaultLogger)({
level: options
.logLevel,
});
}
get status() {
return this.devcycleClient.isInitialized
? server_sdk_1.ProviderStatus.READY
: server_sdk_1.ProviderStatus.NOT_READY;
}
async initialize(context) {
if (this.devcycleClient instanceof index_1.DevCycleCloudClient)
return;
await this.devcycleClient.onClientInitialized();
}
async onClose() {
if (this.devcycleClient instanceof index_1.DevCycleCloudClient)
return;
await this.devcycleClient.close();
}
track(trackingEventName, context, trackingEventDetails) {
if (!context) {
throw new server_sdk_1.TargetingKeyMissingError('Missing context to track event to DevCycle');
}
this.devcycleClient.track(this.devcycleUserFromContext(context), {
type: trackingEventName,
value: trackingEventDetails === null || trackingEventDetails === void 0 ? void 0 : trackingEventDetails.value,
metaData: trackingEventDetails && {
...trackingEventDetails,
value: undefined,
},
});
}
/**
* Generic function to retrieve a DVC variable and convert it to a ResolutionDetails.
* @param flagKey
* @param defaultValue
* @param context
* @private
*/
async getDVCVariable(flagKey, defaultValue, context) {
const dvcVariable = this.devcycleClient.variable(this.devcycleUserFromContext(context), flagKey, defaultValue);
return this.resultFromDVCVariable(dvcVariable instanceof Promise ? await dvcVariable : dvcVariable);
}
/**
* Resolve a boolean OpenFeature flag and its evaluation details.
* @param flagKey
* @param defaultValue
* @param context
*/
async resolveBooleanEvaluation(flagKey, defaultValue, context) {
return this.getDVCVariable(flagKey, defaultValue, context);
}
/**
* Resolve a string OpenFeature flag and its evaluation details.
* @param flagKey
* @param defaultValue
* @param context
*/
async resolveStringEvaluation(flagKey, defaultValue, context) {
return this.getDVCVariable(flagKey, defaultValue, context);
}
/**
* Resolve a number OpenFeature flag and its evaluation details.
* @param flagKey
* @param defaultValue
* @param context
*/
async resolveNumberEvaluation(flagKey, defaultValue, context) {
return this.getDVCVariable(flagKey, defaultValue, context);
}
/**
* Resolve a object OpenFeature flag and its evaluation details.
* @param flagKey
* @param defaultValue
* @param context
*/
async resolveObjectEvaluation(flagKey, defaultValue, context) {
return this.getDVCVariable(flagKey, this.defaultValueFromJsonValue(defaultValue), context);
}
/**
* Convert a OpenFeature JsonValue default value into DVCJSON default value for evaluation.
* @param jsonValue
* @private
*/
defaultValueFromJsonValue(jsonValue) {
if (typeof jsonValue !== 'object' || Array.isArray(jsonValue)) {
throw new server_sdk_1.ParseError('DevCycle only supports object values for JSON flags');
}
if (!jsonValue) {
throw new server_sdk_1.ParseError('DevCycle does not support null default values for JSON flags');
}
// Hard casting here because our DVCJSON typing enforces a flat object when we actually support
// a JSON Object of any depth. Will be fixed soon.
return jsonValue;
}
/**
* Convert a DVCVariable result into a OpenFeature ResolutionDetails.
* TODO: add support for variant / reason / and more error codes from DVC.
* @param variable
* @private
*/
resultFromDVCVariable(variable) {
var _a, _b;
const resolutionDetails = {
value: variable.value,
//TODO: once eval enabled from cloud bucketing, eval won't be null unless defaulted
reason: (_b = (_a = variable.eval) === null || _a === void 0 ? void 0 : _a.reason) !== null && _b !== void 0 ? _b : (variable.isDefaulted
? server_sdk_1.StandardResolutionReasons.DEFAULT
: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH),
};
if (variable.eval) {
const { details, target_id } = variable.eval;
const metadata = {};
if (details)
metadata.evalReasonDetails = details;
if (target_id)
metadata.evalReasonTargetId = target_id;
resolutionDetails.flagMetadata = metadata;
}
return resolutionDetails;
}
/**
* Convert an OpenFeature EvaluationContext into a DevCycleUser.
* @param context
* @private
*/
devcycleUserFromContext(context) {
let user_id, user_id_source;
if (context.targetingKey) {
user_id = context.targetingKey;
user_id_source = 'targetingKey';
}
else if (context.user_id) {
user_id = context.user_id;
user_id_source = 'user_id';
}
else if (context.userId) {
user_id = context.userId;
user_id_source = 'userId';
}
if (!user_id || user_id === '') {
throw new server_sdk_1.TargetingKeyMissingError('DevCycle: Evaluation context does not contain a valid ' +
'targetingKey, user_id, or userId string attribute');
}
if (typeof user_id !== 'string') {
throw new server_sdk_1.TargetingKeyMissingError(`DevCycle: ${user_id_source} must be a string, got ${typeof user_id}`);
}
const dvcUserData = {};
let customData = {};
let privateCustomData = {};
for (const [key, value] of Object.entries(context)) {
// Skip user ID fields as they're handled above
if (key === 'targetingKey' || key === 'user_id' || key === 'userId')
continue;
const knownValueType = DVCKnownPropertyKeyTypes[key];
if (knownValueType) {
if (typeof value !== knownValueType) {
this.logger.warn(`Expected DevCycleUser property "${key}" to be "${knownValueType}" ` +
`but got "${typeof value}" in EvaluationContext. Ignoring value.`);
continue;
}
switch (knownValueType) {
case 'string':
dvcUserData[key] = value;
break;
case 'number':
dvcUserData[key] = value;
break;
case 'object':
if (key === 'privateCustomData') {
privateCustomData = this.convertToDVCCustomDataJSON(value);
}
else if (key === 'customData') {
customData = {
...customData,
...this.convertToDVCCustomDataJSON(value),
};
}
break;
default:
break;
}
}
else {
switch (typeof value) {
case 'string':
customData[key] = value;
break;
case 'number':
customData[key] = value;
break;
case 'boolean':
customData[key] = value;
break;
case 'object':
if (value === null) {
customData[key] = null;
break;
}
this.logger.warn(`EvaluationContext property "${key}" is an ${Array.isArray(value) ? 'Array' : 'Object'}. ` +
'DevCycleUser only supports flat customData properties of type ' +
'string / number / boolean / null');
break;
default:
this.logger.warn(`Unknown EvaluationContext property "${key}" type. ` +
'DevCycleUser only supports flat customData properties of type ' +
'string / number / boolean / null');
break;
}
}
}
return new index_1.DevCycleUser({
user_id,
customData: Object.keys(customData).length ? customData : undefined,
privateCustomData: Object.keys(privateCustomData).length
? privateCustomData
: undefined,
...dvcUserData,
});
}
/**
* Convert customData from an OpenFeature EvaluationContextObject into a DevCycleUser customData.
* @param evaluationData
* @private
*/
convertToDVCCustomDataJSON(evaluationData) {
const customData = {};
for (const [key, value] of Object.entries(evaluationData)) {
switch (typeof value) {
case 'string':
customData[key] = value;
break;
case 'number':
customData[key] = value;
break;
case 'boolean':
customData[key] = value;
break;
case 'undefined':
customData[key] = null;
break;
default:
this.logger.warn(`EvaluationContext property "customData" contains "${key}" property of type ${typeof value}. ` +
'DevCycleUser only supports flat customData properties of type ' +
'string / number / boolean / null');
break;
}
}
return customData;
}
}
exports.DevCycleProvider = DevCycleProvider;
//# sourceMappingURL=DevCycleProvider.js.map
;