remote-config.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * Copyright 2020 Google Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.RemoteConfig = void 0;
  20. const validator = require("../utils/validator");
  21. const remote_config_api_client_internal_1 = require("./remote-config-api-client-internal");
  22. const condition_evaluator_internal_1 = require("./condition-evaluator-internal");
  23. const value_impl_1 = require("./internal/value-impl");
  24. /**
  25. * The Firebase `RemoteConfig` service interface.
  26. */
  27. class RemoteConfig {
  28. /**
  29. * @param app - The app for this RemoteConfig service.
  30. * @constructor
  31. * @internal
  32. */
  33. constructor(app) {
  34. this.app = app;
  35. this.client = new remote_config_api_client_internal_1.RemoteConfigApiClient(app);
  36. }
  37. /**
  38. * Gets the current active version of the {@link RemoteConfigTemplate} of the project.
  39. *
  40. * @returns A promise that fulfills with a `RemoteConfigTemplate`.
  41. */
  42. getTemplate() {
  43. return this.client.getTemplate()
  44. .then((templateResponse) => {
  45. return new RemoteConfigTemplateImpl(templateResponse);
  46. });
  47. }
  48. /**
  49. * Gets the requested version of the {@link RemoteConfigTemplate} of the project.
  50. *
  51. * @param versionNumber - Version number of the Remote Config template to look up.
  52. *
  53. * @returns A promise that fulfills with a `RemoteConfigTemplate`.
  54. */
  55. getTemplateAtVersion(versionNumber) {
  56. return this.client.getTemplateAtVersion(versionNumber)
  57. .then((templateResponse) => {
  58. return new RemoteConfigTemplateImpl(templateResponse);
  59. });
  60. }
  61. /**
  62. * Validates a {@link RemoteConfigTemplate}.
  63. *
  64. * @param template - The Remote Config template to be validated.
  65. * @returns A promise that fulfills with the validated `RemoteConfigTemplate`.
  66. */
  67. validateTemplate(template) {
  68. return this.client.validateTemplate(template)
  69. .then((templateResponse) => {
  70. return new RemoteConfigTemplateImpl(templateResponse);
  71. });
  72. }
  73. /**
  74. * Publishes a Remote Config template.
  75. *
  76. * @param template - The Remote Config template to be published.
  77. * @param options - Optional options object when publishing a Remote Config template:
  78. * - `force`: Setting this to `true` forces the Remote Config template to
  79. * be updated and circumvent the ETag. This approach is not recommended
  80. * because it risks causing the loss of updates to your Remote Config
  81. * template if multiple clients are updating the Remote Config template.
  82. * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates |
  83. * ETag usage and forced updates}.
  84. *
  85. * @returns A Promise that fulfills with the published `RemoteConfigTemplate`.
  86. */
  87. publishTemplate(template, options) {
  88. return this.client.publishTemplate(template, options)
  89. .then((templateResponse) => {
  90. return new RemoteConfigTemplateImpl(templateResponse);
  91. });
  92. }
  93. /**
  94. * Rolls back a project's published Remote Config template to the specified version.
  95. * A rollback is equivalent to getting a previously published Remote Config
  96. * template and re-publishing it using a force update.
  97. *
  98. * @param versionNumber - The version number of the Remote Config template to roll back to.
  99. * The specified version number must be lower than the current version number, and not have
  100. * been deleted due to staleness. Only the last 300 versions are stored.
  101. * All versions that correspond to non-active Remote Config templates (that is, all except the
  102. * template that is being fetched by clients) are also deleted if they are more than 90 days old.
  103. * @returns A promise that fulfills with the published `RemoteConfigTemplate`.
  104. */
  105. rollback(versionNumber) {
  106. return this.client.rollback(versionNumber)
  107. .then((templateResponse) => {
  108. return new RemoteConfigTemplateImpl(templateResponse);
  109. });
  110. }
  111. /**
  112. * Gets a list of Remote Config template versions that have been published, sorted in reverse
  113. * chronological order. Only the last 300 versions are stored.
  114. * All versions that correspond to non-active Remote Config templates (i.e., all except the
  115. * template that is being fetched by clients) are also deleted if they are older than 90 days.
  116. *
  117. * @param options - Optional options object for getting a list of versions.
  118. * @returns A promise that fulfills with a `ListVersionsResult`.
  119. */
  120. listVersions(options) {
  121. return this.client.listVersions(options)
  122. .then((listVersionsResponse) => {
  123. return {
  124. versions: listVersionsResponse.versions?.map(version => new VersionImpl(version)) ?? [],
  125. nextPageToken: listVersionsResponse.nextPageToken,
  126. };
  127. });
  128. }
  129. /**
  130. * Creates and returns a new Remote Config template from a JSON string.
  131. *
  132. * @param json - The JSON string to populate a Remote Config template.
  133. *
  134. * @returns A new template instance.
  135. */
  136. createTemplateFromJSON(json) {
  137. if (!validator.isNonEmptyString(json)) {
  138. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'JSON string must be a valid non-empty string');
  139. }
  140. let template;
  141. try {
  142. template = JSON.parse(json);
  143. }
  144. catch (e) {
  145. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Failed to parse the JSON string: ${json}. ` + e);
  146. }
  147. return new RemoteConfigTemplateImpl(template);
  148. }
  149. /**
  150. * Instantiates {@link ServerTemplate} and then fetches and caches the latest
  151. * template version of the project.
  152. */
  153. async getServerTemplate(options) {
  154. const template = this.initServerTemplate(options);
  155. await template.load();
  156. return template;
  157. }
  158. /**
  159. * Synchronously instantiates {@link ServerTemplate}.
  160. */
  161. initServerTemplate(options) {
  162. const template = new ServerTemplateImpl(this.client, new condition_evaluator_internal_1.ConditionEvaluator(), options?.defaultConfig);
  163. if (options?.template) {
  164. template.set(options?.template);
  165. }
  166. return template;
  167. }
  168. }
  169. exports.RemoteConfig = RemoteConfig;
  170. /**
  171. * Remote Config template internal implementation.
  172. */
  173. class RemoteConfigTemplateImpl {
  174. constructor(config) {
  175. if (!validator.isNonNullObject(config) ||
  176. !validator.isNonEmptyString(config.etag)) {
  177. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Invalid Remote Config template: ${JSON.stringify(config)}`);
  178. }
  179. this.etagInternal = config.etag;
  180. if (typeof config.parameters !== 'undefined') {
  181. if (!validator.isNonNullObject(config.parameters)) {
  182. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config parameters must be a non-null object');
  183. }
  184. this.parameters = config.parameters;
  185. }
  186. else {
  187. this.parameters = {};
  188. }
  189. if (typeof config.parameterGroups !== 'undefined') {
  190. if (!validator.isNonNullObject(config.parameterGroups)) {
  191. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config parameter groups must be a non-null object');
  192. }
  193. this.parameterGroups = config.parameterGroups;
  194. }
  195. else {
  196. this.parameterGroups = {};
  197. }
  198. if (typeof config.conditions !== 'undefined') {
  199. if (!validator.isArray(config.conditions)) {
  200. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config conditions must be an array');
  201. }
  202. this.conditions = config.conditions;
  203. }
  204. else {
  205. this.conditions = [];
  206. }
  207. if (typeof config.version !== 'undefined') {
  208. this.version = new VersionImpl(config.version);
  209. }
  210. }
  211. /**
  212. * Gets the ETag of the template.
  213. *
  214. * @returns The ETag of the Remote Config template.
  215. */
  216. get etag() {
  217. return this.etagInternal;
  218. }
  219. /**
  220. * Returns a JSON-serializable representation of this object.
  221. *
  222. * @returns A JSON-serializable representation of this object.
  223. */
  224. toJSON() {
  225. return {
  226. conditions: this.conditions,
  227. parameters: this.parameters,
  228. parameterGroups: this.parameterGroups,
  229. etag: this.etag,
  230. version: this.version,
  231. };
  232. }
  233. }
  234. /**
  235. * Remote Config dataplane template data implementation.
  236. */
  237. class ServerTemplateImpl {
  238. constructor(apiClient, conditionEvaluator, defaultConfig = {}) {
  239. this.apiClient = apiClient;
  240. this.conditionEvaluator = conditionEvaluator;
  241. this.defaultConfig = defaultConfig;
  242. this.stringifiedDefaultConfig = {};
  243. // RC stores all remote values as string, but it's more intuitive
  244. // to declare default values with specific types, so this converts
  245. // the external declaration to an internal string representation.
  246. for (const key in defaultConfig) {
  247. this.stringifiedDefaultConfig[key] = String(defaultConfig[key]);
  248. }
  249. }
  250. /**
  251. * Fetches and caches the current active version of the project's {@link ServerTemplate}.
  252. */
  253. load() {
  254. return this.apiClient.getServerTemplate()
  255. .then((template) => {
  256. this.cache = new ServerTemplateDataImpl(template);
  257. });
  258. }
  259. /**
  260. * Parses a {@link ServerTemplateDataType} and caches it.
  261. */
  262. set(template) {
  263. let parsed;
  264. if (validator.isString(template)) {
  265. try {
  266. parsed = JSON.parse(template);
  267. }
  268. catch (e) {
  269. // Transforms JSON parse errors to Firebase error.
  270. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Failed to parse the JSON string: ${template}. ` + e);
  271. }
  272. }
  273. else {
  274. parsed = template;
  275. }
  276. // Throws template parse errors.
  277. this.cache = new ServerTemplateDataImpl(parsed);
  278. }
  279. /**
  280. * Evaluates the current template in cache to produce a {@link ServerConfig}.
  281. */
  282. evaluate(context = {}) {
  283. if (!this.cache) {
  284. // This is the only place we should throw during evaluation, since it's under the
  285. // control of application logic. To preserve forward-compatibility, we should only
  286. // return false in cases where the SDK is unsure how to evaluate the fetched template.
  287. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('failed-precondition', 'No Remote Config Server template in cache. Call load() before calling evaluate().');
  288. }
  289. const evaluatedConditions = this.conditionEvaluator.evaluateConditions(this.cache.conditions, context);
  290. const configValues = {};
  291. // Initializes config Value objects with default values.
  292. for (const key in this.stringifiedDefaultConfig) {
  293. configValues[key] = new value_impl_1.ValueImpl('default', this.stringifiedDefaultConfig[key]);
  294. }
  295. // Overlays config Value objects derived by evaluating the template.
  296. for (const [key, parameter] of Object.entries(this.cache.parameters)) {
  297. const { conditionalValues, defaultValue } = parameter;
  298. // Supports parameters with no conditional values.
  299. const normalizedConditionalValues = conditionalValues || {};
  300. let parameterValueWrapper = undefined;
  301. // Iterates in order over condition list. If there is a value associated
  302. // with a condition, this checks if the condition is true.
  303. for (const [conditionName, conditionEvaluation] of evaluatedConditions) {
  304. if (normalizedConditionalValues[conditionName] && conditionEvaluation) {
  305. parameterValueWrapper = normalizedConditionalValues[conditionName];
  306. break;
  307. }
  308. }
  309. if (parameterValueWrapper && parameterValueWrapper.useInAppDefault) {
  310. // TODO: add logging once we have a wrapped logger.
  311. continue;
  312. }
  313. if (parameterValueWrapper) {
  314. const parameterValue = parameterValueWrapper.value;
  315. configValues[key] = new value_impl_1.ValueImpl('remote', parameterValue);
  316. continue;
  317. }
  318. if (!defaultValue) {
  319. // TODO: add logging once we have a wrapped logger.
  320. continue;
  321. }
  322. if (defaultValue.useInAppDefault) {
  323. // TODO: add logging once we have a wrapped logger.
  324. continue;
  325. }
  326. const parameterDefaultValue = defaultValue.value;
  327. configValues[key] = new value_impl_1.ValueImpl('remote', parameterDefaultValue);
  328. }
  329. return new ServerConfigImpl(configValues);
  330. }
  331. /**
  332. * @returns JSON representation of the server template
  333. */
  334. toJSON() {
  335. return this.cache;
  336. }
  337. }
  338. class ServerConfigImpl {
  339. constructor(configValues) {
  340. this.configValues = configValues;
  341. }
  342. getBoolean(key) {
  343. return this.getValue(key).asBoolean();
  344. }
  345. getNumber(key) {
  346. return this.getValue(key).asNumber();
  347. }
  348. getString(key) {
  349. return this.getValue(key).asString();
  350. }
  351. getValue(key) {
  352. return this.configValues[key] || new value_impl_1.ValueImpl('static');
  353. }
  354. }
  355. /**
  356. * Remote Config dataplane template data implementation.
  357. */
  358. class ServerTemplateDataImpl {
  359. constructor(template) {
  360. if (!validator.isNonNullObject(template) ||
  361. !validator.isNonEmptyString(template.etag)) {
  362. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Invalid Remote Config template: ${JSON.stringify(template)}`);
  363. }
  364. this.etag = template.etag;
  365. if (typeof template.parameters !== 'undefined') {
  366. if (!validator.isNonNullObject(template.parameters)) {
  367. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config parameters must be a non-null object');
  368. }
  369. this.parameters = template.parameters;
  370. }
  371. else {
  372. this.parameters = {};
  373. }
  374. if (typeof template.conditions !== 'undefined') {
  375. if (!validator.isArray(template.conditions)) {
  376. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config conditions must be an array');
  377. }
  378. this.conditions = template.conditions;
  379. }
  380. else {
  381. this.conditions = [];
  382. }
  383. if (typeof template.version !== 'undefined') {
  384. this.version = new VersionImpl(template.version);
  385. }
  386. }
  387. }
  388. /**
  389. * Remote Config Version internal implementation.
  390. */
  391. class VersionImpl {
  392. constructor(version) {
  393. if (!validator.isNonNullObject(version)) {
  394. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Invalid Remote Config version instance: ${JSON.stringify(version)}`);
  395. }
  396. if (typeof version.versionNumber !== 'undefined') {
  397. if (!validator.isNonEmptyString(version.versionNumber) &&
  398. !validator.isNumber(version.versionNumber)) {
  399. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version number must be a non-empty string in int64 format or a number');
  400. }
  401. if (!Number.isInteger(Number(version.versionNumber))) {
  402. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version number must be an integer or a string in int64 format');
  403. }
  404. this.versionNumber = version.versionNumber;
  405. }
  406. if (typeof version.updateOrigin !== 'undefined') {
  407. if (!validator.isNonEmptyString(version.updateOrigin)) {
  408. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update origin must be a non-empty string');
  409. }
  410. this.updateOrigin = version.updateOrigin;
  411. }
  412. if (typeof version.updateType !== 'undefined') {
  413. if (!validator.isNonEmptyString(version.updateType)) {
  414. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update type must be a non-empty string');
  415. }
  416. this.updateType = version.updateType;
  417. }
  418. if (typeof version.updateUser !== 'undefined') {
  419. if (!validator.isNonNullObject(version.updateUser)) {
  420. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update user must be a non-null object');
  421. }
  422. this.updateUser = version.updateUser;
  423. }
  424. if (typeof version.description !== 'undefined') {
  425. if (!validator.isNonEmptyString(version.description)) {
  426. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version description must be a non-empty string');
  427. }
  428. this.description = version.description;
  429. }
  430. if (typeof version.rollbackSource !== 'undefined') {
  431. if (!validator.isNonEmptyString(version.rollbackSource)) {
  432. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version rollback source must be a non-empty string');
  433. }
  434. this.rollbackSource = version.rollbackSource;
  435. }
  436. if (typeof version.isLegacy !== 'undefined') {
  437. if (!validator.isBoolean(version.isLegacy)) {
  438. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version.isLegacy must be a boolean');
  439. }
  440. this.isLegacy = version.isLegacy;
  441. }
  442. // The backend API provides timestamps in ISO date strings. The Admin SDK exposes timestamps
  443. // in UTC date strings. If a developer uses a previously obtained template with UTC timestamps
  444. // we could still validate it below.
  445. if (typeof version.updateTime !== 'undefined') {
  446. if (!this.isValidTimestamp(version.updateTime)) {
  447. throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update time must be a valid date string');
  448. }
  449. this.updateTime = new Date(version.updateTime).toUTCString();
  450. }
  451. }
  452. /**
  453. * @returns A JSON-serializable representation of this object.
  454. */
  455. toJSON() {
  456. return {
  457. versionNumber: this.versionNumber,
  458. updateOrigin: this.updateOrigin,
  459. updateType: this.updateType,
  460. updateUser: this.updateUser,
  461. description: this.description,
  462. rollbackSource: this.rollbackSource,
  463. isLegacy: this.isLegacy,
  464. updateTime: this.updateTime,
  465. };
  466. }
  467. isValidTimestamp(timestamp) {
  468. // This validation fails for timestamps earlier than January 1, 1970 and considers strings
  469. // such as "1.2" as valid timestamps.
  470. return validator.isNonEmptyString(timestamp) && (new Date(timestamp)).getTime() > 0;
  471. }
  472. }