Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,14 @@ declare namespace admin.remoteConfig {
*/
parameters: { [key: string]: RemoteConfigParameter };

/**
* Map of parameter group names to their parameter group objects.
* A group's name is mutable but must be unique among groups in the Remote Config template.
* The name is limited to 256 characters and intended to be human-readable. Any Unicode
* characters are allowed.
*/
parameterGroups: { [key: string]: RemoteConfigParameterGroup };

/**
* ETag of the current Remote Config template (readonly).
*/
Expand Down Expand Up @@ -866,6 +874,27 @@ declare namespace admin.remoteConfig {
description?: string;
}

/**
* Interface representing a Remote Config parameter group.
* Grouping parameters is only for management purposes and does not affect client-side
* fetching of parameter values.
*/
export interface RemoteConfigParameterGroup {
/**
* A description for the group. Its length must be less than or equal to 256 characters.
* A description may contain any Unicode characters.
*/
description?: string;

/**
* Map of parameter keys to their optional default values and optional conditional values for
* parameters that belong to this group. A parameter only appears once per
* Remote Config template. An ungrouped parameter appears at the top level, whereas a
* parameter organized within a group appears within its group's map of parameters.
*/
parameters: { [key: string]: RemoteConfigParameter };
}

/**
* Interface representing a Remote Config condition.
* A condition targets a specific group of users. A list of these conditions make up
Expand Down
18 changes: 17 additions & 1 deletion src/remote-config/remote-config-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export interface RemoteConfigParameter {
description?: string;
}

/** Interface representing a Remote Config parameter group. */
export interface RemoteConfigParameterGroup {
description?: string;
parameters: { [key: string]: RemoteConfigParameter };
}

/** Interface representing a Remote Config condition. */
export interface RemoteConfigCondition {
name: string;
Expand All @@ -76,6 +82,7 @@ export interface RemoteConfigCondition {
export interface RemoteConfigTemplate {
conditions: RemoteConfigCondition[];
parameters: { [key: string]: RemoteConfigParameter };
parameterGroups: { [key: string]: RemoteConfigParameterGroup };
readonly etag: string;
}

Expand Down Expand Up @@ -118,6 +125,7 @@ export class RemoteConfigApiClient {
return {
conditions: resp.data.conditions,
parameters: resp.data.parameters,
parameterGroups: resp.data.parameterGroups,
etag: resp.headers['etag'],
};
})
Expand All @@ -138,6 +146,7 @@ export class RemoteConfigApiClient {
return {
conditions: resp.data.conditions,
parameters: resp.data.parameters,
parameterGroups: resp.data.parameterGroups,
// validating a template returns an etag with the suffix -0 means that your update
// was successfully validated. We set the etag back to the original etag of the template
// to allow future operations.
Expand Down Expand Up @@ -167,6 +176,7 @@ export class RemoteConfigApiClient {
return {
conditions: resp.data.conditions,
parameters: resp.data.parameters,
parameterGroups: resp.data.parameterGroups,
etag: resp.headers['etag'],
};
})
Expand All @@ -189,6 +199,7 @@ export class RemoteConfigApiClient {
data: {
conditions: template.conditions,
parameters: template.parameters,
parameterGroups: template.parameterGroups,
}
};
return this.httpClient.send(request);
Expand Down Expand Up @@ -245,7 +256,7 @@ export class RemoteConfigApiClient {

/**
* Checks if the given RemoteConfigTemplate object is valid.
* The object must have valid parameters, conditions, and an etag.
* The object must have valid parameters, parameter groups, conditions, and an etag.
*
* @param {RemoteConfigTemplate} template A RemoteConfigTemplate object to be validated.
*/
Expand All @@ -265,6 +276,11 @@ export class RemoteConfigApiClient {
'invalid-argument',
'Remote Config parameters must be a non-null object');
}
if (!validator.isNonNullObject(template.parameterGroups)) {
throw new FirebaseRemoteConfigError(
'invalid-argument',
'Remote Config parameter groups must be a non-null object');
}
if (!validator.isArray(template.conditions)) {
throw new FirebaseRemoteConfigError(
'invalid-argument',
Expand Down
13 changes: 13 additions & 0 deletions src/remote-config/remote-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
RemoteConfigTemplate,
RemoteConfigParameter,
RemoteConfigCondition,
RemoteConfigParameterGroup,
} from './remote-config-api-client';

/**
Expand Down Expand Up @@ -131,6 +132,7 @@ export class RemoteConfig implements FirebaseServiceInterface {
class RemoteConfigTemplateImpl implements RemoteConfigTemplate {

public parameters: { [key: string]: RemoteConfigParameter };
public parameterGroups: { [key: string]: RemoteConfigParameterGroup };
public conditions: RemoteConfigCondition[];
private readonly etagInternal: string;

Expand All @@ -155,6 +157,17 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
this.parameters = {};
}

if (typeof config.parameterGroups !== 'undefined') {
if (!validator.isNonNullObject(config.parameterGroups)) {
throw new FirebaseRemoteConfigError(
'invalid-argument',
'Remote Config parameter groups must be a non-null object');
}
this.parameterGroups = config.parameterGroups;
} else {
this.parameterGroups = {};
}

if (typeof config.conditions !== 'undefined') {
if (!validator.isArray(config.conditions)) {
throw new FirebaseRemoteConfigError(
Expand Down
32 changes: 29 additions & 3 deletions test/integration/remote-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,35 @@ const VALID_PARAMETERS = {
defaultValue: { value: 'welcome text' + Date.now() },
conditionalValues: {
ios: { value: 'welcome ios text' },
andriod: { value: 'welcome andriod text' },
android: { value: 'welcome android text' },
},
}
};

const VALID_PARAMETER_GROUPS = {
// eslint-disable-next-line @typescript-eslint/camelcase
new_menu: {
description: 'Description of the group.',
parameters: {
// eslint-disable-next-line @typescript-eslint/camelcase
pumpkin_spice_season: {
defaultValue: { value: 'A Gryffindor must love a pumpkin spice latte.' },
conditionalValues: {
'android': { value: 'A Droid must love a pumpkin spice latte.' },
},
description: 'Description of the parameter.',
},
},
},
};

const VALID_CONDITIONS: admin.remoteConfig.RemoteConfigCondition[] = [{
name: 'ios',
expression: 'device.os == \'ios\'',
tagColor: 'INDIGO',
},
{
name: 'andriod',
name: 'android',
expression: 'device.os == \'android\'',
tagColor: 'GREEN',
}];
Expand All @@ -67,22 +84,25 @@ describe('admin.remoteConfig', () => {

describe('validateTemplate', () => {
it('should succeed with a vaild template', () => {
// set parameters and conditions
// set parameters, groups, and conditions
currentTemplate.conditions = VALID_CONDITIONS;
currentTemplate.parameters = VALID_PARAMETERS;
currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS;
return admin.remoteConfig().validateTemplate(currentTemplate)
.then((template) => {
expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/);
expect(template.conditions.length).to.equal(2);
expect(template.conditions).to.deep.equal(VALID_CONDITIONS);
expect(template.parameters).to.deep.equal(VALID_PARAMETERS);
expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS);
});
});

it('should propagate API errors', () => {
// rejects with invalid-argument when conditions used in parameters do not exist
currentTemplate.conditions = [];
currentTemplate.parameters = VALID_PARAMETERS;
currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS;
return admin.remoteConfig().validateTemplate(currentTemplate)
.should.eventually.be.rejected.and.have.property('code', 'remote-config/invalid-argument');
});
Expand All @@ -93,19 +113,22 @@ describe('admin.remoteConfig', () => {
// set parameters and conditions
currentTemplate.conditions = VALID_CONDITIONS;
currentTemplate.parameters = VALID_PARAMETERS;
currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS;
return admin.remoteConfig().publishTemplate(currentTemplate)
.then((template) => {
expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/);
expect(template.conditions.length).to.equal(2);
expect(template.conditions).to.deep.equal(VALID_CONDITIONS);
expect(template.parameters).to.deep.equal(VALID_PARAMETERS);
expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS);
});
});

it('should propagate API errors', () => {
// rejects with invalid-argument when conditions used in parameters do not exist
currentTemplate.conditions = [];
currentTemplate.parameters = VALID_PARAMETERS;
currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS;
return admin.remoteConfig().publishTemplate(currentTemplate)
.should.eventually.be.rejected.and.have.property('code', 'remote-config/invalid-argument');
});
Expand All @@ -119,6 +142,7 @@ describe('admin.remoteConfig', () => {
expect(template.conditions.length).to.equal(2);
expect(template.conditions).to.deep.equal(VALID_CONDITIONS);
expect(template.parameters).to.deep.equal(VALID_PARAMETERS);
expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS);
});
});
});
Expand All @@ -144,6 +168,7 @@ describe('admin.remoteConfig', () => {
const invalidEtags = [...INVALID_STRINGS];
const sourceTemplate = {
parameters: VALID_PARAMETERS,
parameterGroups: VALID_PARAMETER_GROUPS,
conditions: VALID_CONDITIONS,
etag: 'etag-1234-1',
};
Expand All @@ -170,6 +195,7 @@ describe('admin.remoteConfig', () => {
expect(newTemplate.conditions.length).to.equal(2);
expect(newTemplate.conditions).to.deep.equal(VALID_CONDITIONS);
expect(newTemplate.parameters).to.deep.equal(VALID_PARAMETERS);
expect(newTemplate.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS);
});
});
});
32 changes: 28 additions & 4 deletions test/unit/remote-config/remote-config-api-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('RemoteConfigApiClient', () => {
const TEST_RESPONSE = {
conditions: [{ name: 'ios', expression: 'exp' }],
parameters: { param: { defaultValue: { value: 'true' } } },
parameterGroups: { group: { parameters: { paramabc: { defaultValue: { value: 'true' } } }, } },
version: {},
};

Expand Down Expand Up @@ -86,6 +87,22 @@ describe('RemoteConfigApiClient', () => {
description: 'this is a promo',
},
},
parameterGroups: {
// eslint-disable-next-line @typescript-eslint/camelcase
new_menu: {
description: 'Description of the group.',
parameters: {
// eslint-disable-next-line @typescript-eslint/camelcase
pumpkin_spice_season: {
defaultValue: { value: 'A Gryffindor must love a pumpkin spice latte.' },
conditionalValues: {
'android_en': { value: 'A Droid must love a pumpkin spice latte.' },
},
description: 'Description of the parameter.',
},
},
},
},
etag: 'etag-123456789012-6',
};

Expand Down Expand Up @@ -127,6 +144,7 @@ describe('RemoteConfigApiClient', () => {
.then((resp) => {
expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions);
expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters);
expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups);
expect(resp.etag).to.equal('etag-123456789012-1');
expect(stub).to.have.been.calledOnce.and.calledWith({
method: 'GET',
Expand Down Expand Up @@ -203,6 +221,7 @@ describe('RemoteConfigApiClient', () => {
.then((resp) => {
expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions);
expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters);
expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups);
// validate template returns an etag with the suffix -0 when successful.
// verify that the etag matches the original template etag.
expect(resp.etag).to.equal('etag-123456789012-6');
Expand All @@ -213,6 +232,7 @@ describe('RemoteConfigApiClient', () => {
data: {
conditions: REMOTE_CONFIG_TEMPLATE.conditions,
parameters: REMOTE_CONFIG_TEMPLATE.parameters,
parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups,
}
});
});
Expand All @@ -230,8 +250,9 @@ describe('RemoteConfigApiClient', () => {

[null, undefined, ''].forEach((etag) => {
it('should reject when the etag in template is null, undefined, or an empty string', () => {
expect(() => apiClient.validateTemplate({ conditions: [], parameters: {}, etag: etag as any }))
.to.throw('ETag must be a non-empty string.');
expect(() => apiClient.validateTemplate({
conditions: [], parameters: {}, parameterGroups: {}, etag: etag as any
})).to.throw('ETag must be a non-empty string.');
});
});

Expand Down Expand Up @@ -305,6 +326,7 @@ describe('RemoteConfigApiClient', () => {
.then((resp) => {
expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions);
expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters);
expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups);
expect(resp.etag).to.equal('etag-123456789012-6');
expect(stub).to.have.been.calledOnce.and.calledWith({
method: 'PUT',
Expand All @@ -313,6 +335,7 @@ describe('RemoteConfigApiClient', () => {
data: {
conditions: REMOTE_CONFIG_TEMPLATE.conditions,
parameters: REMOTE_CONFIG_TEMPLATE.parameters,
parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups,
}
});
});
Expand All @@ -331,8 +354,9 @@ describe('RemoteConfigApiClient', () => {

[null, undefined, ''].forEach((etag) => {
it('should reject when the etag in template is null, undefined, or an empty string', () => {
expect(() => apiClient.publishTemplate({ conditions: [], parameters: {}, etag: etag as any }))
.to.throw('ETag must be a non-empty string.');
expect(() => apiClient.publishTemplate({
conditions: [], parameters: {}, parameterGroups: {}, etag: etag as any
})).to.throw('ETag must be a non-empty string.');
});
});

Expand Down
Loading