Skip to content

Commit 769f7f7

Browse files
Merge pull request #17 from configcat/datagovernance
Datagovernance
2 parents f4e4fc1 + cb4854e commit 769f7f7

29 files changed

+501
-77
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 configcat
3+
Copyright (c) 2020 configcat
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "configcat-common",
3-
"version": "3.0.2",
3+
"version": "4.0.0",
44
"description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

src/AutoPollConfigService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class AutoPollConfigService extends ConfigServiceBase implements IConfigS
1818

1919
getConfig(): Promise<ProjectConfig> {
2020

21-
var p: ProjectConfig = this.cache.Get(this.baseConfig.apiKey);
21+
var p: ProjectConfig = this.cache.get(this.baseConfig.getCacheKey());
2222

2323
if (!p) {
2424
return this.refreshLogic();
@@ -34,7 +34,7 @@ export class AutoPollConfigService extends ConfigServiceBase implements IConfigS
3434
private refreshLogic(): Promise<ProjectConfig> {
3535
return new Promise(async resolve => {
3636

37-
let p: ProjectConfig = this.cache.Get(this.baseConfig.apiKey);
37+
let p: ProjectConfig = this.cache.get(this.baseConfig.getCacheKey());
3838
const newConfig = await this.refreshLogicBaseAsync(p)
3939

4040
if (!p || p.HttpETag !== newConfig.HttpETag) {

src/Cache.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { ICache } from "./index";
44
export class InMemoryCache implements ICache {
55
cache: { [apiKey: string] : ProjectConfig; } = {};
66

7-
Set(apiKey: string, config: ProjectConfig): void {
8-
this.cache[apiKey] = config;
7+
set(key: string, config: ProjectConfig): void {
8+
this.cache[key] = config;
99
}
1010

11-
Get(apiKey: string): ProjectConfig {
12-
return this.cache[apiKey];
11+
get(key: string): ProjectConfig {
12+
return this.cache[key];
1313
}
1414
}

src/ConfigCatClient.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AutoPollConfigService } from "./AutoPollConfigService";
55
import { LazyLoadConfigService } from "./LazyLoadConfigService";
66
import { ManualPollService } from "./ManualPollService";
77
import { User, IRolloutEvaluator, RolloutEvaluator } from "./RolloutEvaluator";
8-
import { Setting, RolloutRules, RolloutPercentageItems } from "./ProjectConfig";
8+
import { Setting, RolloutRules, RolloutPercentageItems, ConfigFile } from "./ProjectConfig";
99

1010
export const CONFIG_CHANGE_EVENT_NAME: string = "changed";
1111

@@ -125,13 +125,13 @@ export class ConfigCatClient implements IConfigCatClient {
125125
getAllKeysAsync(): Promise<string[]> {
126126
return new Promise(async (resolve) => {
127127
const config = await this.configService.getConfig();
128-
if (!config || !config.ConfigJSON) {
128+
if (!config || !config.ConfigJSON || !config.ConfigJSON[ConfigFile.FeatureFlags]) {
129129
this.options.logger.error("JSONConfig is not present, returning empty array");
130130
resolve([]);
131131
return;
132132
}
133133

134-
resolve(Object.keys(config.ConfigJSON));
134+
resolve(Object.keys(config.ConfigJSON[ConfigFile.FeatureFlags]));
135135
});
136136
}
137137

@@ -180,19 +180,20 @@ export class ConfigCatClient implements IConfigCatClient {
180180
getKeyAndValueAsync(variationId: string): Promise<SettingKeyValue> {
181181
return new Promise(async (resolve) => {
182182
const config = await this.configService.getConfig();
183-
if (!config || !config.ConfigJSON) {
183+
if (!config || !config.ConfigJSON || !config.ConfigJSON[ConfigFile.FeatureFlags]) {
184184
this.options.logger.error("JSONConfig is not present, returning null");
185185
resolve(null);
186186
return;
187187
}
188188

189-
for (let settingKey in config.ConfigJSON) {
190-
if (variationId === config.ConfigJSON[settingKey][Setting.VariationId]) {
191-
resolve({ settingKey: settingKey, settingValue: config.ConfigJSON[settingKey][Setting.Value] });
189+
const featureFlags = config.ConfigJSON[ConfigFile.FeatureFlags];
190+
for (let settingKey in featureFlags) {
191+
if (variationId === featureFlags[settingKey][Setting.VariationId]) {
192+
resolve({ settingKey: settingKey, settingValue: featureFlags[settingKey][Setting.Value] });
192193
return;
193194
}
194195

195-
const rolloutRules = config.ConfigJSON[settingKey][Setting.RolloutRules];
196+
const rolloutRules = featureFlags[settingKey][Setting.RolloutRules];
196197
if (rolloutRules && rolloutRules.length > 0) {
197198
for (let i: number = 0; i < rolloutRules.length; i++) {
198199
const rolloutRule: any = rolloutRules[i];
@@ -203,7 +204,7 @@ export class ConfigCatClient implements IConfigCatClient {
203204
}
204205
}
205206

206-
const percentageItems = config.ConfigJSON[settingKey][Setting.RolloutPercentageItems];
207+
const percentageItems = featureFlags[settingKey][Setting.RolloutPercentageItems];
207208
if (percentageItems && percentageItems.length > 0) {
208209
for (let i: number = 0; i < percentageItems.length; i++) {
209210
const percentageItem: any = percentageItems[i];

src/ConfigCatClientOptions.ts

+41-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,30 @@ import { ConfigCatConsoleLogger } from "./ConfigCatLogger";
22
import { IConfigCatLogger, IAutoPollOptions, ILazyLoadingOptions, IManualPollOptions, LogLevel } from "./index";
33
import COMMON_VERSION from "./Version";
44

5+
6+
/** Control the location of the config.json files containing your feature flags and settings within the ConfigCat CDN. */
7+
export enum DataGovernance {
8+
/** Select this if your feature flags are published to all global CDN nodes. */
9+
Global = 0,
10+
/** Select this if your feature flags are published to CDN nodes only in the EU. */
11+
EuOnly = 1
12+
}
13+
514
export interface IOptions {
615
logger?: IConfigCatLogger;
716
requestTimeoutMs?: number;
817
baseUrl?: string;
18+
/** You can set a base_url if you want to use a proxy server between your application and ConfigCat */
919
proxy?: string;
20+
/** Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard:
21+
* https://app.configcat.com/organization/data-governance (Only Organization Admins have access) */
22+
dataGovernance?: DataGovernance;
1023
}
1124

1225
export abstract class OptionsBase implements IOptions {
1326

27+
private configFileName = "config_v5";
28+
1429
public logger: IConfigCatLogger = new ConfigCatConsoleLogger(LogLevel.Warn);
1530

1631
public apiKey: string;
@@ -19,25 +34,38 @@ export abstract class OptionsBase implements IOptions {
1934

2035
public requestTimeoutMs: number = 30000;
2136

22-
public baseUrl: string = "https://cdn.configcat.com";
37+
public baseUrl: string;
38+
39+
public baseUrlOverriden: boolean = false;
2340

2441
public proxy: string = "";
2542

43+
public dataGovernance: DataGovernance;
44+
2645
constructor(apiKey: string, clientVersion: string, options: IOptions) {
2746
if (!apiKey) {
2847
throw new Error("Invalid 'apiKey' value");
2948
}
3049

3150
this.apiKey = apiKey;
3251
this.clientVersion = clientVersion;
52+
this.dataGovernance = options?.dataGovernance ?? DataGovernance.Global;
53+
54+
switch (this.dataGovernance) {
55+
case DataGovernance.EuOnly:
56+
this.baseUrl = "https://cdn-eu.configcat.com";
57+
break;
58+
default:
59+
this.baseUrl = "https://cdn-global.configcat.com";
60+
break;
61+
}
3362

34-
if (options)
35-
{
63+
if (options) {
3664
if (options.logger) {
3765
this.logger = options.logger;
3866
}
3967

40-
if (options.requestTimeoutMs ) {
68+
if (options.requestTimeoutMs) {
4169
if (options.requestTimeoutMs < 0) {
4270
throw new Error("Invalid 'requestTimeoutMs' value");
4371
}
@@ -47,6 +75,7 @@ export abstract class OptionsBase implements IOptions {
4775

4876
if (options.baseUrl) {
4977
this.baseUrl = options.baseUrl;
78+
this.baseUrlOverriden = true;
5079
}
5180

5281
if (options.proxy) {
@@ -56,14 +85,20 @@ export abstract class OptionsBase implements IOptions {
5685
}
5786

5887
getUrl(): string {
59-
return this.baseUrl + "/configuration-files/" + this.apiKey + "/config_v4.json";
88+
return this.baseUrl + "/configuration-files/" + this.apiKey + "/" + this.configFileName + ".json";
89+
}
90+
91+
getCacheKey(): string {
92+
return "js_" + this.configFileName + "_" + this.apiKey;
6093
}
6194
}
6295

6396
export class AutoPollOptions extends OptionsBase implements IAutoPollOptions {
6497

98+
/** The client's poll interval in seconds. Default: 60 seconds. */
6599
public pollIntervalSeconds: number = 60;
66100

101+
/** You can subscribe to configuration changes with this callback */
67102
public configChanged: () => void = () => { };
68103

69104
constructor(apiKey: string, options: IAutoPollOptions) {
@@ -95,6 +130,7 @@ export class ManualPollOptions extends OptionsBase implements IManualPollOptions
95130

96131
export class LazyLoadOptions extends OptionsBase implements ILazyLoadingOptions {
97132

133+
/** The cache TTL. */
98134
public cacheTimeToLiveSeconds: number = 60;
99135

100136
constructor(apiKey: string, options: ILazyLoadingOptions) {

src/ConfigServiceBase.ts

+67-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { IConfigFetcher, ICache} from "./index";
1+
import { IConfigFetcher, ICache } from "./index";
22
import { OptionsBase } from "./ConfigCatClientOptions";
3-
import { ProjectConfig } from "./ProjectConfig";
3+
import { ConfigFile, Preferences, ProjectConfig } from "./ProjectConfig";
44

55
export interface IConfigService {
6-
getConfig() : Promise<ProjectConfig>;
6+
getConfig(): Promise<ProjectConfig>;
77

88
refreshConfigAsync(): Promise<ProjectConfig>;
99
}
@@ -24,12 +24,10 @@ export abstract class ConfigServiceBase {
2424

2525
return new Promise(resolve => {
2626

27-
this.configFetcher.fetchLogic(this.baseConfig, lastProjectConfig, (newConfig) => {
28-
29-
if (newConfig) {
30-
31-
this.cache.Set(this.baseConfig.apiKey, newConfig);
27+
this.fetchLogic(this.baseConfig, lastProjectConfig, 0, (newConfig) => {
3228

29+
if (newConfig && newConfig.ConfigJSON) {
30+
this.cache.set(this.baseConfig.getCacheKey(), newConfig);
3331
resolve(newConfig);
3432
}
3533
else {
@@ -39,4 +37,65 @@ export abstract class ConfigServiceBase {
3937
});
4038
});
4139
}
40+
41+
private fetchLogic(options: OptionsBase, lastProjectConfig: ProjectConfig, retries: number, callback: (newProjectConfig: ProjectConfig) => void): void {
42+
this.configFetcher.fetchLogic(this.baseConfig, lastProjectConfig, (newConfig) => {
43+
44+
if (!newConfig || !newConfig.ConfigJSON) {
45+
callback(null);
46+
return;
47+
}
48+
49+
const preferences = newConfig.ConfigJSON[ConfigFile.Preferences];
50+
if (!preferences) {
51+
52+
callback(newConfig);
53+
return;
54+
}
55+
56+
const baseUrl = preferences[Preferences.BaseUrl];
57+
58+
// If the base_url is the same as the last called one, just return the response.
59+
if (!baseUrl || baseUrl == options.baseUrl) {
60+
61+
callback(newConfig);
62+
return;
63+
}
64+
65+
const redirect = preferences[Preferences.Redirect];
66+
67+
// If the base_url is overridden, and the redirect parameter is not 2 (force),
68+
// the SDK should not redirect the calls and it just have to return the response.
69+
if (options.baseUrlOverriden && redirect !== 2) {
70+
callback(newConfig);
71+
return;
72+
}
73+
74+
options.baseUrl = baseUrl;
75+
76+
if (redirect === 0) {
77+
78+
callback(newConfig);
79+
return;
80+
}
81+
82+
if (redirect === 1) {
83+
84+
options.logger.warn("Your dataGovernance parameter at ConfigCatClient initialization is not in sync " +
85+
"with your preferences on the ConfigCat Dashboard: " +
86+
"https://app.configcat.com/organization/data-governance. " +
87+
"Only Organization Admins can access this preference.");
88+
}
89+
90+
if (retries >= 2) {
91+
options.logger.error("Redirect loop during config.json fetch. Please contact [email protected].");
92+
callback(newConfig);
93+
return;
94+
}
95+
96+
this.fetchLogic(options, lastProjectConfig, ++retries, callback);
97+
return;
98+
}
99+
);
100+
}
42101
}

src/LazyLoadConfigService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class LazyLoadConfigService extends ConfigServiceBase implements IConfigS
1616

1717
getConfig(): Promise<ProjectConfig> {
1818

19-
let p: ProjectConfig = this.cache.Get(this.baseConfig.apiKey);
19+
let p: ProjectConfig = this.cache.get(this.baseConfig.getCacheKey());
2020

2121
if (p && p.Timestamp + (this.cacheTimeToLiveSeconds * 1000) > new Date().getTime()) {
2222
return new Promise(resolve => resolve(p));
@@ -27,7 +27,7 @@ export class LazyLoadConfigService extends ConfigServiceBase implements IConfigS
2727

2828
refreshConfigAsync(): Promise<ProjectConfig> {
2929

30-
let p: ProjectConfig = this.cache.Get(this.baseConfig.apiKey);
30+
let p: ProjectConfig = this.cache.get(this.baseConfig.getCacheKey());
3131
return this.refreshLogicBaseAsync(p)
3232
}
3333
}

src/ManualPollService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ export class ManualPollService extends ConfigServiceBase implements IConfigServi
1212

1313
getConfig(): Promise<ProjectConfig> {
1414

15-
return new Promise(resolve => resolve(this.cache.Get(this.baseConfig.apiKey)));
15+
return new Promise(resolve => resolve(this.cache.get(this.baseConfig.getCacheKey())));
1616
}
1717

1818
refreshConfigAsync(): Promise<ProjectConfig> {
1919

20-
let p: ProjectConfig = this.cache.Get(this.baseConfig.apiKey);
20+
let p: ProjectConfig = this.cache.get(this.baseConfig.getCacheKey());
2121
return this.refreshLogicBaseAsync(p)
2222
}
2323
}

src/ProjectConfig.ts

+12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ export class ProjectConfig {
1313
}
1414
}
1515

16+
export class ConfigFile {
17+
static Preferences: string = "p";
18+
19+
static FeatureFlags: string = "f";
20+
}
21+
22+
export class Preferences {
23+
static BaseUrl: string = "u";
24+
25+
static Redirect: string = "r";
26+
}
27+
1628
export class Setting{
1729
static Value: string = "v";
1830

0 commit comments

Comments
 (0)