Skip to content

Commit fa393a6

Browse files
authored
feat(flagd-core): add update config support, returns changed keys (#703)
Signed-off-by: Michael Beemer <[email protected]>
1 parent 82ac6e1 commit fa393a6

File tree

4 files changed

+94
-6
lines changed

4 files changed

+94
-6
lines changed

libs/shared/flagd-core/src/lib/feature-flag.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FlagValue } from '@openfeature/core';
2+
import { createHash } from 'crypto';
23

34
/**
45
* Flagd flag configuration structure mapping to schema definition.
@@ -18,12 +19,18 @@ export class FeatureFlag {
1819
private readonly _defaultVariant: string;
1920
private readonly _variants: Map<string, FlagValue>;
2021
private readonly _targeting: unknown;
22+
private readonly _hash: string;
2123

2224
constructor(flag: Flag) {
2325
this._state = flag['state'];
2426
this._defaultVariant = flag['defaultVariant'];
2527
this._variants = new Map<string, FlagValue>(Object.entries(flag['variants']));
2628
this._targeting = flag['targeting'];
29+
this._hash = createHash('sha1').update(JSON.stringify(flag)).digest('base64');
30+
}
31+
32+
get hash(): string {
33+
return this._hash;
2734
}
2835

2936
get state(): string {

libs/shared/flagd-core/src/lib/flagd-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export class FlagdCore implements Storage {
4040
return this;
4141
}
4242

43-
setConfigurations(cfg: string): void {
44-
this._storage.setConfigurations(cfg);
43+
setConfigurations(cfg: string): string[] {
44+
return this._storage.setConfigurations(cfg);
4545
}
4646

4747
getFlag(key: string): FeatureFlag | undefined {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { FeatureFlag } from './feature-flag';
2+
import { MemoryStorage } from './storage';
3+
4+
describe('MemoryStorage', () => {
5+
let storage: MemoryStorage;
6+
7+
beforeEach(() => {
8+
storage = new MemoryStorage();
9+
});
10+
11+
it('should set configurations correctly', () => {
12+
const cfg =
13+
'{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}';
14+
storage.setConfigurations(cfg);
15+
16+
// Assert that the configurations are set correctly
17+
expect(storage['_flags']).toEqual(
18+
new Map([
19+
[
20+
'flag1',
21+
new FeatureFlag({
22+
state: 'ENABLED',
23+
defaultVariant: 'variant1',
24+
variants: { variant1: true, variant2: false },
25+
}),
26+
],
27+
]),
28+
);
29+
});
30+
31+
it('should update configurations correctly', () => {
32+
const flags1 = `{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}`;
33+
const flags2 = `{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}},"flag2":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}`;
34+
const flags3 = `{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false},"targeting":{"if":[true,"variant1"]}},"flag2":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}},"flag3":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}`;
35+
const flags4 = `{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false},"targeting":{"if":[true,"variant2"]}},"flag2":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}},"flag3":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}`;
36+
37+
expect(storage.setConfigurations(flags1)).toEqual(['flag1']);
38+
expect(storage.setConfigurations(flags2)).toEqual(['flag2']);
39+
expect(storage.setConfigurations(flags3)).toEqual(['flag3', 'flag1']);
40+
expect(storage.setConfigurations(flags4)).toEqual(['flag1']);
41+
});
42+
43+
it('should get flag correctly', () => {
44+
const cfg =
45+
'{"flags":{"flag1":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}},"flag2":{"state":"ENABLED","defaultVariant":"variant1","variants":{"variant1":true,"variant2":false}}}}';
46+
storage.setConfigurations(cfg);
47+
48+
// Assert that the correct flag is returned
49+
expect(storage.getFlag('flag1')).toEqual(
50+
new FeatureFlag({ state: 'ENABLED', defaultVariant: 'variant1', variants: { variant1: true, variant2: false } }),
51+
);
52+
expect(storage.getFlag('flag2')).toEqual(
53+
new FeatureFlag({ state: 'ENABLED', defaultVariant: 'variant1', variants: { variant1: true, variant2: false } }),
54+
);
55+
56+
// Assert that undefined is returned for non-existing flag
57+
expect(storage.getFlag('flag3')).toBeUndefined();
58+
});
59+
});

libs/shared/flagd-core/src/lib/storage.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import { parse } from './parser';
66
*/
77
export interface Storage {
88
/**
9-
* Sets the configurations from the given string.
9+
* Sets the configurations and returns the list of flags that have changed.
1010
* @param cfg The configuration string to be parsed and stored.
11+
* @returns The list of flags that have changed.
1112
* @throws {Error} If the configuration string is invalid.
1213
*/
13-
setConfigurations(cfg: string): void;
14+
setConfigurations(cfg: string): string[];
1415

1516
/**
1617
* Gets the feature flag configuration with the given key.
@@ -44,7 +45,28 @@ export class MemoryStorage implements Storage {
4445
return this._flags;
4546
}
4647

47-
setConfigurations(cfg: string): void {
48-
this._flags = parse(cfg);
48+
setConfigurations(cfg: string): string[] {
49+
const newFlags = parse(cfg);
50+
const oldFlags = this._flags;
51+
const added: string[] = [];
52+
const removed: string[] = [];
53+
const changed: string[] = [];
54+
55+
newFlags.forEach((value, key) => {
56+
if (!oldFlags.has(key)) {
57+
added.push(key);
58+
} else if (oldFlags.get(key)?.hash !== value.hash) {
59+
changed.push(key);
60+
}
61+
});
62+
63+
oldFlags.forEach((_, key) => {
64+
if (!newFlags.has(key)) {
65+
removed.push(key);
66+
}
67+
});
68+
69+
this._flags = newFlags;
70+
return [...added, ...removed, ...changed];
4971
}
5072
}

0 commit comments

Comments
 (0)