Skip to content

Commit 5e68127

Browse files
Update Configs DTO
AI-Session-Id: e9b3e072-2ec0-428a-b108-9646c6de8629 AI-Tool: claude-code AI-Model: unknown
1 parent 8ec6a80 commit 5e68127

10 files changed

Lines changed: 183 additions & 78 deletions

File tree

src/dtos/types.ts

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -215,51 +215,6 @@ export interface IRBSegment {
215215
} | null
216216
}
217217

218-
// Superset of ISplit (i.e., ISplit extends IConfig)
219-
// - with optional fields related to targeting information and
220-
// - an optional link fields that binds configurations to other entities
221-
export interface IConfig {
222-
name: string,
223-
changeNumber: number,
224-
status?: 'ACTIVE' | 'ARCHIVED',
225-
conditions?: ISplitCondition[] | null,
226-
prerequisites?: null | {
227-
n: string,
228-
ts: string[]
229-
}[]
230-
killed?: boolean,
231-
defaultTreatment: string,
232-
trafficTypeName?: string,
233-
seed?: number,
234-
trafficAllocation?: number,
235-
trafficAllocationSeed?: number
236-
configurations?: {
237-
[treatmentName: string]: string
238-
},
239-
sets?: string[],
240-
impressionsDisabled?: boolean,
241-
// a map of entities (e.g., pipeline, feature-flag, etc) to configuration variants
242-
links?: {
243-
[entityType: string]: {
244-
[entityName: string]: string
245-
}
246-
}
247-
}
248-
249-
/** Interface of the parsed JSON response of `/configs` */
250-
export interface IConfigsResponse {
251-
configs?: {
252-
t: number,
253-
s?: number,
254-
d: IConfig[]
255-
},
256-
rbs?: {
257-
t: number,
258-
s?: number,
259-
d: IRBSegment[]
260-
}
261-
}
262-
263218
// @TODO: rename to IDefinition (Configs and Feature Flags are definitions)
264219
export interface ISplit {
265220
name: string,
@@ -277,7 +232,7 @@ export interface ISplit {
277232
trafficAllocation?: number,
278233
trafficAllocationSeed?: number
279234
configurations?: {
280-
[treatmentName: string]: string
235+
[treatmentName: string]: string | SplitIO.JsonObject
281236
},
282237
sets?: string[],
283238
impressionsDisabled?: boolean

src/evaluator/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface IEvaluation {
2222
treatment?: string,
2323
label: string,
2424
changeNumber?: number,
25-
config?: string | null
25+
config?: string | null | SplitIO.JsonObject
2626
}
2727

2828
export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean }

src/sdkClient/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
173173
if (withConfig) {
174174
return {
175175
treatment,
176-
config
176+
config: config as string | null
177177
};
178178
}
179179

src/sdkManager/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null {
2929
killed: splitObject.killed,
3030
changeNumber: splitObject.changeNumber || 0,
3131
treatments: collectTreatments(splitObject),
32-
configs: splitObject.configurations || {},
32+
configs: splitObject.configurations as SplitIO.SplitView['configs'] || {},
3333
sets: splitObject.sets || [],
3434
defaultTreatment: splitObject.defaultTreatment,
3535
impressionsDisabled: splitObject.impressionsDisabled === true,

src/services/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyT
3535

3636
export type IFetchAuth = (userKeys?: string[]) => Promise<IResponse>
3737

38-
export type IFetchSplitChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise<IResponse>
38+
export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise<IResponse>
3939

4040
export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise<IResponse>
4141

@@ -59,8 +59,8 @@ export interface ISplitApi {
5959
getSdkAPIHealthCheck: IHealthCheckAPI
6060
getEventsAPIHealthCheck: IHealthCheckAPI
6161
fetchAuth: IFetchAuth
62-
fetchSplitChanges: IFetchSplitChanges
63-
fetchConfigs: IFetchSplitChanges
62+
fetchSplitChanges: IFetchDefinitionChanges
63+
fetchConfigs: IFetchDefinitionChanges
6464
fetchSegmentChanges: IFetchSegmentChanges
6565
fetchMemberships: IFetchMemberships
6666
postEventsBulk: IPostEventsBulk
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ISplitChangesResponse } from '../../../../dtos/types';
2+
import { convertConfigsResponseToDefinitionChangesResponse, IConfigsResponse } from '../configsFetcher';
3+
4+
// TODO: complete input and output mocks
5+
const inputMock: IConfigsResponse = {
6+
s: 100,
7+
t: 200,
8+
d: [{ 'name': 'SomeConfig1', 'defaultVariant': 'v2', 'variants': [{ 'name': 'v1', 'definition': { 'prop1': true, 'prop2': 123 } }, { 'name': 'v2', 'definition': { 'prop1': false, 'prop2': 456 } }], 'targeting': { 'conditions': [{ 'variant': 'v1', 'label': 'main condition', 'matchers': [{ 'type': 'IS_EQUAL_TO', 'data': { 'type': 'NUMBER', 'number': 42 }, 'attribute': 'age' }, { 'type': 'WHITELIST', 'data': { 'strings': ['a', 'b', 'c'] }, 'attribute': 'favoriteCharacter' }] }] } }],
9+
};
10+
11+
const expectedOutput: ISplitChangesResponse = {
12+
ff: {
13+
s: 100,
14+
t: 200,
15+
d: [{
16+
name: 'SomeConfig1',
17+
changeNumber: 0,
18+
status: 'ACTIVE',
19+
killed: false,
20+
defaultTreatment: 'v2',
21+
trafficTypeName: 'user',
22+
seed: 0,
23+
configurations: {
24+
'v1': { 'prop1': true, 'prop2': 123 },
25+
'v2': { 'prop1': false, 'prop2': 456 },
26+
},
27+
conditions: [
28+
{
29+
conditionType: 'WHITELIST',
30+
label: 'main condition',
31+
matcherGroup: {
32+
combiner: 'AND',
33+
matchers: [
34+
{
35+
matcherType: 'EQUAL_TO',
36+
negate: false,
37+
keySelector: { trafficType: 'user', attribute: 'age' },
38+
unaryNumericMatcherData: { dataType: 'NUMBER', value: 42 },
39+
},
40+
{
41+
matcherType: 'WHITELIST',
42+
negate: false,
43+
keySelector: { trafficType: 'user', attribute: 'favoriteCharacter' },
44+
whitelistMatcherData: { whitelist: ['a', 'b', 'c'] },
45+
},
46+
],
47+
},
48+
partitions: [{ treatment: 'v1', size: 100 }],
49+
},
50+
{
51+
conditionType: 'ROLLOUT',
52+
matcherGroup: {
53+
combiner: 'AND',
54+
matchers: [{
55+
keySelector: null,
56+
matcherType: 'ALL_KEYS',
57+
negate: false,
58+
}],
59+
},
60+
partitions: [{ treatment: 'v2', size: 100 }],
61+
label: 'default rule',
62+
},
63+
],
64+
}],
65+
},
66+
};
67+
68+
describe('convertConfigsResponseToDefinitionChangesResponse', () => {
69+
70+
test('should convert a configs response to a definition changes response', () => {
71+
const result = convertConfigsResponseToDefinitionChangesResponse(inputMock);
72+
expect(result).toEqual(expectedOutput);
73+
});
74+
75+
});
Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,63 @@
1-
import { IConfig, IConfigsResponse, ISplitChangesResponse, ISplitCondition } from '../../../dtos/types';
2-
import { IFetchSplitChanges, IResponse } from '../../../services/types';
1+
import { ISplit, ISplitChangesResponse, ISplitCondition, ISplitMatcher } from '../../../dtos/types';
2+
import { IFetchDefinitionChanges, IResponse } from '../../../services/types';
33
import { ISplitChangesFetcher } from './types';
4+
import SplitIO from '../../../../types/splitio';
5+
6+
type IConfigMatcher = {
7+
type: 'IS_EQUAL_TO';
8+
data: { type: 'NUMBER'; number: number };
9+
attribute?: string;
10+
} | {
11+
type: 'WHITELIST';
12+
data: { strings: string[] };
13+
attribute?: string;
14+
}
15+
16+
type IConfig = {
17+
name: string;
18+
variants: Array<{
19+
name: string;
20+
definition: SplitIO.JsonObject;
21+
}>;
22+
defaultVariant: string;
23+
changeNumber?: number;
24+
targeting?: {
25+
conditions?: Array<{
26+
variant: string;
27+
label: string;
28+
matchers: Array<IConfigMatcher>;
29+
}>
30+
};
31+
}
32+
33+
/** Interface of the parsed JSON response of `/configs` */
34+
export type IConfigsResponse = {
35+
t: number,
36+
s?: number,
37+
d: IConfig[]
38+
}
439

540
/**
641
* Factory of Configs fetcher.
742
* Configs fetcher is a wrapper around `configs` API service that parses the response and handle errors.
843
*/
9-
export function configsFetcherFactory(fetchConfigs: IFetchSplitChanges): ISplitChangesFetcher {
44+
export function configsFetcherFactory(fetchConfigs: IFetchDefinitionChanges): ISplitChangesFetcher {
1045

1146
return function configsFetcher(
1247
since: number,
1348
noCache?: boolean,
1449
till?: number,
1550
rbSince?: number,
16-
// Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker
51+
// Optional decorator for `fetchConfigs` promise, such as timeout or time tracker
1752
decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
1853
): Promise<ISplitChangesResponse> {
1954

2055
let configsPromise = fetchConfigs(since, noCache, till, rbSince);
2156
if (decorator) configsPromise = decorator(configsPromise);
2257

2358
return configsPromise
24-
.then((resp: IResponse) => resp.json())
25-
.then(convertConfigsResponseToSplitChangesResponse);
59+
.then<IConfigsResponse>((resp: IResponse) => resp.json())
60+
.then(convertConfigsResponseToDefinitionChangesResponse);
2661
};
2762

2863
}
@@ -43,28 +78,64 @@ function defaultCondition(treatment: string): ISplitCondition {
4378
};
4479
}
4580

46-
function convertConfigToDefinitionDTO(config: IConfig) {
47-
const defaultTreatment = config.defaultTreatment || 'default';
81+
function convertMatcher(matcher: IConfigMatcher): ISplitMatcher {
82+
const keySelector = matcher.attribute ? { trafficType: 'user', attribute: matcher.attribute } : null;
83+
84+
switch (matcher.type) {
85+
case 'IS_EQUAL_TO':
86+
return {
87+
matcherType: 'EQUAL_TO',
88+
negate: false,
89+
keySelector,
90+
unaryNumericMatcherData: { dataType: matcher.data.type, value: matcher.data.number },
91+
};
92+
case 'WHITELIST':
93+
return {
94+
matcherType: 'WHITELIST',
95+
negate: false,
96+
keySelector,
97+
whitelistMatcherData: { whitelist: matcher.data.strings },
98+
};
99+
}
100+
}
101+
102+
function convertConfigToDefinition(config: IConfig): ISplit {
103+
const defaultTreatment = config.defaultVariant || (config.variants && config.variants[0]?.name) || 'control';
104+
105+
const configurations: Record<string, SplitIO.JsonObject> = {};
106+
config.variants.forEach(variant => configurations[variant.name] = variant.definition);
107+
108+
const conditions: ISplitCondition[] = config.targeting?.conditions?.map(condition => ({
109+
conditionType: condition.matchers.some((m: IConfigMatcher) => m.type === 'WHITELIST') ? 'WHITELIST' : 'ROLLOUT',
110+
label: condition.label,
111+
matcherGroup: {
112+
combiner: 'AND',
113+
matchers: condition.matchers.map(convertMatcher),
114+
},
115+
partitions: [{ treatment: condition.variant, size: 100 }],
116+
})) || [];
117+
118+
conditions.push(defaultCondition(defaultTreatment));
48119

49120
return {
50-
...config,
121+
name: config.name,
122+
changeNumber: config.changeNumber || 0,
123+
status: 'ACTIVE',
124+
conditions,
125+
killed: false,
51126
defaultTreatment,
52-
trafficTypeName: config.trafficTypeName || 'user',
53-
conditions: config.conditions && config.conditions.length > 0 ? config.conditions : [defaultCondition(defaultTreatment)],
54-
killed: config.killed || false,
55-
seed: config.seed || 0,
56-
trafficAllocation: config.trafficAllocation || 100,
57-
trafficAllocationSeed: config.trafficAllocationSeed || 0,
127+
trafficTypeName: 'user',
128+
seed: 0,
129+
configurations,
58130
};
59131
}
60132

61-
function convertConfigsResponseToSplitChangesResponse(configs: IConfigsResponse): ISplitChangesResponse {
133+
export function convertConfigsResponseToDefinitionChangesResponse(configs: IConfigsResponse): ISplitChangesResponse {
62134
return {
63-
...configs,
64-
ff: configs.configs ? {
65-
...configs.configs,
66-
d: configs.configs.d?.map(convertConfigToDefinitionDTO)
67-
} : undefined,
68-
rbs: configs.rbs
135+
ff: {
136+
s: configs.s,
137+
t: configs.t,
138+
d: configs.d.map(convertConfigToDefinition),
139+
},
69140
};
70141
}

src/sync/polling/fetchers/splitChangesFetcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ISettings } from '../../../types';
22
import { ISplitChangesResponse } from '../../../dtos/types';
3-
import { IFetchSplitChanges, IResponse } from '../../../services/types';
3+
import { IFetchDefinitionChanges, IResponse } from '../../../services/types';
44
import { IStorageBase } from '../../../storages/types';
55
import { FLAG_SPEC_VERSION } from '../../../utils/constants';
66
import { base } from '../../../utils/settingsValidation';
@@ -20,7 +20,7 @@ function sdkEndpointOverridden(settings: ISettings) {
2020
* SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors.
2121
*/
2222
// @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation
23-
export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick<IStorageBase, 'splits' | 'rbSegments'>): ISplitChangesFetcher {
23+
export function splitChangesFetcherFactory(fetchSplitChanges: IFetchDefinitionChanges, settings: ISettings, storage: Pick<IStorageBase, 'splits' | 'rbSegments'>): ISplitChangesFetcher {
2424

2525
const log = settings.log;
2626
const PROXY_CHECK_INTERVAL_MILLIS = checkIfServerSide(settings) ? PROXY_CHECK_INTERVAL_MILLIS_SS : PROXY_CHECK_INTERVAL_MILLIS_CS;

src/sync/polling/syncTasks/splitsSyncTask.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { IReadinessManager } from '../../../readiness/types';
33
import { syncTaskFactory } from '../../syncTask';
44
import { ISplitsSyncTask } from '../types';
55
import { splitChangesFetcherFactory } from '../fetchers/splitChangesFetcher';
6-
import { IFetchSplitChanges } from '../../../services/types';
6+
import { IFetchDefinitionChanges } from '../../../services/types';
77
import { ISettings } from '../../../types';
88
import { splitChangesUpdaterFactory } from '../updaters/splitChangesUpdater';
99

1010
/**
1111
* Creates a sync task that periodically executes a `splitChangesUpdater` task
1212
*/
1313
export function splitsSyncTaskFactory(
14-
fetchSplitChanges: IFetchSplitChanges,
14+
fetchSplitChanges: IFetchDefinitionChanges,
1515
storage: IStorageSync,
1616
readiness: IReadinessManager,
1717
settings: ISettings,

types/splitio.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,4 +2284,8 @@ declare namespace SplitIO {
22842284
*/
22852285
split(featureFlagName: string): SplitViewAsync;
22862286
}
2287+
2288+
type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
2289+
type JsonArray = JsonValue[];
2290+
type JsonObject = { [key: string]: JsonValue; };
22872291
}

0 commit comments

Comments
 (0)