Skip to content

Commit

Permalink
Minor improvements and fixes (#100)
Browse files Browse the repository at this point in the history
* Don't swallow exceptions when config json deserialization fails

* Add types describing the config json schema and use them in the config model builder logic to eliminate loose typing + correct minor errors in the config model types

* Fix additional maxInitWaitTime tests by adding a bit of tolerance to timing checks

* Fix getKeyAndValueAsync so it doesn't return an unsupported setting value

* Bump version

* Run tests on Node 20

* Improve message of error 1103

* Fix another occasionally failing test (increase tolerance of timing checks)

* Correct the generic parameter constraint of IEvaluationDetails and SettingKey
  • Loading branch information
adams85 authored Jan 8, 2024
1 parent 988dbc3 commit 1ec2efe
Show file tree
Hide file tree
Showing 16 changed files with 371 additions and 209 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/common-js-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: [ 14, 16, 18 ]
node: [ 14, 16, 18, 20 ]
os: [ macos-latest, ubuntu-latest, windows-latest ]
fail-fast: false
name: Test [${{ matrix.os }}, Node ${{ matrix.node }}]
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "configcat-common",
"version": "9.0.0",
"version": "9.1.0",
"description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions samples/deno-sandbox/import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"../../src/ConfigCatClientOptions": "../../src/ConfigCatClientOptions.ts",
"../../src/ConfigCatLogger": "../../src/ConfigCatLogger.ts",
"../../src/ConfigFetcher": "../../src/ConfigFetcher.ts",
"../../src/ConfigJson": "../../src/ConfigJson.ts",
"../../src/ConfigServiceBase": "../../src/ConfigServiceBase.ts",
"../../src/DefaultEventEmitter": "../../src/DefaultEventEmitter.ts",
"../../src/EvaluateLogBuilder": "../../src/EvaluateLogBuilder.ts",
Expand Down
16 changes: 10 additions & 6 deletions src/ConfigCatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ManualPollConfigService } from "./ManualPollConfigService";
import { getWeakRefStub, isWeakRefAvailable } from "./Polyfills";
import type { IConfig, PercentageOption, ProjectConfig, Setting, SettingValue } from "./ProjectConfig";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf } from "./RolloutEvaluator";
import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, isAllowedValue } from "./RolloutEvaluator";
import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, handleInvalidReturnValue, isAllowedValue } from "./RolloutEvaluator";
import type { User } from "./User";
import { errorToString, isArray, throwError } from "./Utils";

Expand Down Expand Up @@ -496,7 +496,7 @@ export class ConfigCatClient implements IConfigCatClient {

for (const [settingKey, setting] of Object.entries(settings)) {
if (variationId === setting.variationId) {
return new SettingKeyValue(settingKey, setting.value);
return new SettingKeyValue(settingKey, ensureAllowedValue(setting.value));
}

const targetingRules = settings[settingKey].targetingRules;
Expand All @@ -507,12 +507,12 @@ export class ConfigCatClient implements IConfigCatClient {
for (let j = 0; j < then.length; j++) {
const percentageOption: PercentageOption = then[j];
if (variationId === percentageOption.variationId) {
return new SettingKeyValue(settingKey, percentageOption.value);
return new SettingKeyValue(settingKey, ensureAllowedValue(percentageOption.value));
}
}
}
else if (variationId === then.variationId) {
return new SettingKeyValue(settingKey, then.value);
return new SettingKeyValue(settingKey, ensureAllowedValue(then.value));
}
}
}
Expand All @@ -522,7 +522,7 @@ export class ConfigCatClient implements IConfigCatClient {
for (let i = 0; i < percentageOptions.length; i++) {
const percentageOption: PercentageOption = percentageOptions[i];
if (variationId === percentageOption.variationId) {
return new SettingKeyValue(settingKey, percentageOption.value);
return new SettingKeyValue(settingKey, ensureAllowedValue(percentageOption.value));
}
}
}
Expand Down Expand Up @@ -759,7 +759,7 @@ class Snapshot implements IConfigCatClientSnapshot {
}

/** Setting key-value pair. */
export class SettingKeyValue<TValue = SettingValue> {
export class SettingKeyValue<TValue extends SettingValue = SettingValue> {
constructor(
public settingKey: string,
public settingValue: TValue) { }
Expand Down Expand Up @@ -794,6 +794,10 @@ function ensureAllowedDefaultValue(value: SettingValue): void {
}
}

function ensureAllowedValue(value: NonNullable<SettingValue>): NonNullable<SettingValue> {
return isAllowedValue(value) ? value : handleInvalidReturnValue(value);
}

/* GC finalization support */

// Defines the interface of the held value which is passed to ConfigCatClient.finalize by FinalizationRegistry.
Expand Down
7 changes: 4 additions & 3 deletions src/ConfigCatLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class LoggerWrapper implements IConfigCatLogger {
fetchFailedDueToUnexpectedError(ex: any): LogMessage {
return this.log(
LogLevel.Error, 1103,
"Unexpected error occurred while trying to fetch config JSON.",
"Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP.",
ex
);
}
Expand All @@ -207,10 +207,11 @@ export class LoggerWrapper implements IConfigCatLogger {
);
}

fetchReceived200WithInvalidBody(): LogMessage {
fetchReceived200WithInvalidBody(ex: any): LogMessage {
return this.log(
LogLevel.Error, 1105,
"Fetching config JSON was successful but the HTTP response content was invalid."
"Fetching config JSON was successful but the HTTP response content was invalid.",
ex
);
}

Expand Down
258 changes: 258 additions & 0 deletions src/ConfigJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* The ConfigCat config_v6.json schema that is used by the ConfigCat SDKs, described using TypeScript types.
*/

export type Config = {
/**
* Preferences of the config.json, mostly for controlling the redirection behaviour of the SDK.
*/
p: Preferences;
/**
* Segment definitions for re-using segment rules in targeting rules.
*/
s?: Segment[];
/**
* Setting definitions.
*/
f?: { [key: string]: SettingUnion };
}

export type Preferences = {
/**
* The redirect mode that should be used in case the data governance mode is wrongly configured.
*/
r: RedirectMode;
/**
* The base url from where the config.json is intended to be downloaded.
*/
u: string;
/**
* The salt that, combined with the feature flag key or segment name, is used to hash values for sensitive text comparisons.
*/
s: string;
}

export type Segment = {
n: string;
r: [UserConditionUnion, ...UserConditionUnion[]];
}

export type SettingUnion = { [K in SettingType]: Setting<K> }[SettingType];

export type Setting<TSetting extends SettingType = SettingType> = {
t: TSetting;
/**
* The percentage rule evaluation will hash this attribute of the User object to calculate the buckets.
*/
a?: string;
r?: TargetingRule<TSetting>[];
p?: PercentageOption<TSetting>[];
} & ServedValue<TSetting>;

export type TargetingRule<TSetting extends SettingType = SettingType> = {
c: [ConditionUnion, ...ConditionUnion[]];
} & (
{ s: ServedValue<TSetting>; p?: never }
| { p: PercentageOption<TSetting>[]; s?: never }
)

export type ConditionUnion =
{ u: UserConditionUnion; p?: never; s?: never }
| { p: PrerequisiteFlagCondition; u?: never; s?: never }
| { s: SegmentCondition; u?: never; p?: never }

export type PercentageOption<TSetting extends SettingType = SettingType> = {
p: number;
} & ServedValue<TSetting>;

export type SettingValue<TSetting extends SettingType = SettingType> = {
[SettingType.Boolean]: { b: boolean; s?: never; i?: never; d?: never };
[SettingType.String]: { s: string; b?: never; i?: never; d?: never };
[SettingType.Int]: { i: number; b?: never; s?: never; d?: never };
[SettingType.Double]: { d: number; b?: never; s?: never; i?: never };
}[TSetting];

export type UserConditionUnion = { [K in UserComparator]: UserCondition<K> }[UserComparator];

export type UserCondition<TComparator extends UserComparator = UserComparator> = {
/**
* The attribute of the user object that should be used to evalaute this rule.
*/
a: string;
c: TComparator;
} & UserConditionComparisonValue<TComparator>

export type UserConditionComparisonValue<TComparator extends UserComparator = UserComparator> = {
[UserComparator.IsOneOf]: UserConditionStringListComparisonValue;
[UserComparator.IsNotOneOf]: UserConditionStringListComparisonValue;
[UserComparator.ContainsAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.NotContainsAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SemVerIsOneOf]: UserConditionStringListComparisonValue;
[UserComparator.SemVerIsNotOneOf]: UserConditionStringListComparisonValue;
[UserComparator.SemVerLess]: UserConditionStringComparisonValue;
[UserComparator.SemVerLessOrEquals]: UserConditionStringComparisonValue;
[UserComparator.SemVerGreater]: UserConditionStringComparisonValue;
[UserComparator.SemVerGreaterOrEquals]: UserConditionStringComparisonValue;
[UserComparator.NumberEquals]: UserConditionNumberComparisonValue;
[UserComparator.NumberNotEquals]: UserConditionNumberComparisonValue;
[UserComparator.NumberLess]: UserConditionNumberComparisonValue;
[UserComparator.NumberLessOrEquals]: UserConditionNumberComparisonValue;
[UserComparator.NumberGreater]: UserConditionNumberComparisonValue;
[UserComparator.NumberGreaterOrEquals]: UserConditionNumberComparisonValue;
[UserComparator.SensitiveIsOneOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveIsNotOneOf]: UserConditionStringListComparisonValue;
[UserComparator.DateTimeBefore]: UserConditionNumberComparisonValue;
[UserComparator.DateTimeAfter]: UserConditionNumberComparisonValue;
[UserComparator.SensitiveTextEquals]: UserConditionStringComparisonValue;
[UserComparator.SensitiveTextNotEquals]: UserConditionStringComparisonValue;
[UserComparator.SensitiveTextStartsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveTextNotStartsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveTextEndsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveTextNotEndsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveArrayContainsAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.SensitiveArrayNotContainsAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.TextEquals]: UserConditionStringComparisonValue;
[UserComparator.TextNotEquals]: UserConditionStringComparisonValue;
[UserComparator.TextStartsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.TextNotStartsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.TextEndsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.TextNotEndsWithAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.ArrayContainsAnyOf]: UserConditionStringListComparisonValue;
[UserComparator.ArrayNotContainsAnyOf]: UserConditionStringListComparisonValue;
}[TComparator];

export type UserConditionStringComparisonValue = { s: string; d?: never; l?: never };
export type UserConditionNumberComparisonValue = { d: number; s?: never; l?: never };
export type UserConditionStringListComparisonValue = { l: string[]; s?: never; d?: never };

export type PrerequisiteFlagCondition = {
/**
* The key of the prerequisite flag.
*/
f: string;
c: PrerequisiteFlagComparator;
v: SettingValue;
}

export type SegmentCondition = {
/**
* The zero-based index of the segment.
*/
s: number;
c: SegmentComparator;
}

export type ServedValue<TSetting extends SettingType = SettingType> = {
v: SettingValue<TSetting>;
i: string;
}

export enum RedirectMode {
No = 0,
Should = 1,
Force = 2,
}

/** Setting type. */
export enum SettingType {
/** On/off type (feature flag). */
Boolean = 0,
/** Text type. */
String = 1,
/** Whole number type. */
Int = 2,
/** Decimal number type. */
Double = 3,
}

/** User Object attribute comparison operator used during the evaluation process. */
export enum UserComparator {
/** IS ONE OF (cleartext) - It matches when the comparison attribute is equal to any of the comparison values. */
IsOneOf = 0,
/** IS NOT ONE OF (cleartext) - It matches when the comparison attribute is not equal to any of the comparison values. */
IsNotOneOf = 1,
/** CONTAINS ANY OF (cleartext) - It matches when the comparison attribute contains any comparison values as a substring. */
ContainsAnyOf = 2,
/** NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute does not contain any comparison values as a substring. */
NotContainsAnyOf = 3,
/** IS ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is equal to any of the comparison values. */
SemVerIsOneOf = 4,
/** IS NOT ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is not equal to any of the comparison values. */
SemVerIsNotOneOf = 5,
/** &lt; (semver) - It matches when the comparison attribute interpreted as a semantic version is less than the comparison value. */
SemVerLess = 6,
/** &lt;= (semver) - It matches when the comparison attribute interpreted as a semantic version is less than or equal to the comparison value. */
SemVerLessOrEquals = 7,
/** &gt; (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than the comparison value. */
SemVerGreater = 8,
/** &gt;= (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than or equal to the comparison value. */
SemVerGreaterOrEquals = 9,
/** = (number) - It matches when the comparison attribute interpreted as a decimal number is equal to the comparison value. */
NumberEquals = 10,
/** != (number) - It matches when the comparison attribute interpreted as a decimal number is not equal to the comparison value. */
NumberNotEquals = 11,
/** &lt; (number) - It matches when the comparison attribute interpreted as a decimal number is less than the comparison value. */
NumberLess = 12,
/** &lt;= (number) - It matches when the comparison attribute interpreted as a decimal number is less than or equal to the comparison value. */
NumberLessOrEquals = 13,
/** &gt; (number) - It matches when the comparison attribute interpreted as a decimal number is greater than the comparison value. */
NumberGreater = 14,
/** &gt;= (number) - It matches when the comparison attribute interpreted as a decimal number is greater than or equal to the comparison value. */
NumberGreaterOrEquals = 15,
/** IS ONE OF (hashed) - It matches when the comparison attribute is equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveIsOneOf = 16,
/** IS NOT ONE OF (hashed) - It matches when the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveIsNotOneOf = 17,
/** BEFORE (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since <see href="https://en.wikipedia.org/wiki/Unix_time">Unix Epoch</see> is less than the comparison value. */
DateTimeBefore = 18,
/** AFTER (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since <see href="https://en.wikipedia.org/wiki/Unix_time">Unix Epoch</see> is greater than the comparison value. */
DateTimeAfter = 19,
/** EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextEquals = 20,
/** NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextNotEquals = 21,
/** STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextStartsWithAnyOf = 22,
/** NOT STARTS WITH ANY OF (hashed) - It matches when the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextNotStartsWithAnyOf = 23,
/** ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextEndsWithAnyOf = 24,
/** NOT ENDS WITH ANY OF (hashed) - It matches when the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveTextNotEndsWithAnyOf = 25,
/** ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveArrayContainsAnyOf = 26,
/** ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */
SensitiveArrayNotContainsAnyOf = 27,
/** EQUALS (cleartext) - It matches when the comparison attribute is equal to the comparison value. */
TextEquals = 28,
/** NOT EQUALS (cleartext) - It matches when the comparison attribute is not equal to the comparison value. */
TextNotEquals = 29,
/** STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute starts with any of the comparison values. */
TextStartsWithAnyOf = 30,
/** NOT STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute does not start with any of the comparison values. */
TextNotStartsWithAnyOf = 31,
/** ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute ends with any of the comparison values. */
TextEndsWithAnyOf = 32,
/** NOT ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute does not end with any of the comparison values. */
TextNotEndsWithAnyOf = 33,
/** ARRAY CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */
ArrayContainsAnyOf = 34,
/** ARRAY NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */
ArrayNotContainsAnyOf = 35,
}

/** Prerequisite flag comparison operator used during the evaluation process. */
export enum PrerequisiteFlagComparator {
/** EQUALS - It matches when the evaluated value of the specified prerequisite flag is equal to the comparison value. */
Equals = 0,
/** NOT EQUALS - It matches when the evaluated value of the specified prerequisite flag is not equal to the comparison value. */
NotEquals = 1
}

/** Segment comparison operator used during the evaluation process. */
export enum SegmentComparator {
/** IS IN SEGMENT - It matches when the conditions of the specified segment are evaluated to true. */
IsIn = 0,
/** IS NOT IN SEGMENT - It matches when the conditions of the specified segment are evaluated to false. */
IsNotIn = 1,
}
Loading

0 comments on commit 1ec2efe

Please sign in to comment.