firebase-tools 11.2.1 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/lib/apiv2.js +5 -0
  2. package/lib/checkValidTargetFilters.js +3 -2
  3. package/lib/commands/ext-export.js +2 -0
  4. package/lib/database/rulesConfig.js +35 -8
  5. package/lib/deploy/extensions/planner.js +1 -0
  6. package/lib/deploy/extensions/prepare.js +18 -1
  7. package/lib/deploy/extensions/release.js +4 -0
  8. package/lib/deploy/functions/build.js +43 -52
  9. package/lib/deploy/functions/params.js +189 -0
  10. package/lib/deploy/functions/prepare.js +1 -1
  11. package/lib/deploy/functions/release/index.js +4 -0
  12. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +3 -0
  13. package/lib/deploy/functions/runtimes/node/parseTriggers.js +1 -1
  14. package/lib/deploy/hosting/convertConfig.js +76 -16
  15. package/lib/deploy/index.js +1 -1
  16. package/lib/deploy/lifecycleHooks.js +1 -1
  17. package/lib/deploy/storage/prepare.js +29 -6
  18. package/lib/downloadUtils.js +1 -1
  19. package/lib/emulator/controller.js +0 -1
  20. package/lib/emulator/downloadableEmulators.js +9 -9
  21. package/lib/emulator/functionsEmulator.js +9 -1
  22. package/lib/emulator/functionsEmulatorRuntime.js +68 -58
  23. package/lib/emulator/functionsRuntimeWorker.js +35 -9
  24. package/lib/emulator/storage/apis/gcloud.js +1 -1
  25. package/lib/extensions/etags.js +28 -0
  26. package/lib/extensions/warnings.js +7 -1
  27. package/lib/functions/env.js +51 -2
  28. package/lib/functionsConfig.js +1 -1
  29. package/lib/gcp/iam.js +20 -17
  30. package/lib/gcp/rules.js +2 -3
  31. package/lib/gcp/runtimeconfig.js +1 -1
  32. package/lib/hosting/api.js +9 -11
  33. package/lib/hosting/expireUtils.js +2 -2
  34. package/lib/management/projects.js +6 -7
  35. package/lib/profileReport.js +16 -14
  36. package/lib/rc.js +14 -10
  37. package/lib/requireConfig.js +6 -6
  38. package/lib/rulesDeploy.js +1 -1
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +2 -1
package/lib/apiv2.js CHANGED
@@ -15,6 +15,7 @@ const responseToError_1 = require("./responseToError");
15
15
  const FormData = require("form-data");
16
16
  const pkg = require("../package.json");
17
17
  const CLI_VERSION = pkg.version;
18
+ const GOOG_QUOTA_USER = "x-goog-quota-user";
18
19
  let accessToken = "";
19
20
  let refreshToken = "";
20
21
  function setRefreshToken(token = "") {
@@ -309,6 +310,10 @@ class Client {
309
310
  }
310
311
  const logURL = this.requestURL(options);
311
312
  logger_1.logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`);
313
+ const headers = options.headers;
314
+ if (headers && headers.has(GOOG_QUOTA_USER)) {
315
+ logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${headers.get(GOOG_QUOTA_USER) || ""}`);
316
+ }
312
317
  if (options.body !== undefined) {
313
318
  let logBody = "[omitted]";
314
319
  if (!((_b = options.skipLog) === null || _b === void 0 ? void 0 : _b.body)) {
@@ -19,6 +19,7 @@ function targetsHaveFilters(...targets) {
19
19
  function targetsHaveNoFilters(...targets) {
20
20
  return targets.some((t) => !t.includes(":"));
21
21
  }
22
+ const FILTERABLE_TARGETS = new Set(["hosting", "functions", "firestore", "storage", "database"]);
22
23
  async function checkValidTargetFilters(options) {
23
24
  const only = !options.only ? [] : options.only.split(",");
24
25
  return new Promise((resolve, reject) => {
@@ -28,10 +29,10 @@ async function checkValidTargetFilters(options) {
28
29
  if (options.except) {
29
30
  return reject(new error_1.FirebaseError("Cannot specify both --only and --except"));
30
31
  }
31
- const nonFilteredTypes = deploy_1.VALID_DEPLOY_TARGETS.filter((t) => !["hosting", "functions"].includes(t));
32
+ const nonFilteredTypes = deploy_1.VALID_DEPLOY_TARGETS.filter((t) => !FILTERABLE_TARGETS.has(t));
32
33
  const targetsForNonFilteredTypes = targetsForTypes(only, ...nonFilteredTypes);
33
34
  if (targetsForNonFilteredTypes.length && targetsHaveFilters(...targetsForNonFilteredTypes)) {
34
- return reject(new error_1.FirebaseError("Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions and hosting"));
35
+ return reject(new error_1.FirebaseError("Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions, hosting, storage, and firestore"));
35
36
  }
36
37
  const targetsForFunctions = targetsForTypes(only, "functions");
37
38
  if (targetsForFunctions.length &&
@@ -4,6 +4,7 @@ exports.command = void 0;
4
4
  const checkMinRequiredVersion_1 = require("../checkMinRequiredVersion");
5
5
  const command_1 = require("../command");
6
6
  const planner = require("../deploy/extensions/planner");
7
+ const etags_1 = require("../extensions/etags");
7
8
  const export_1 = require("../extensions/export");
8
9
  const extensionsHelper_1 = require("../extensions/extensionsHelper");
9
10
  const manifest = require("../extensions/manifest");
@@ -63,4 +64,5 @@ exports.command = new command_1.Command("ext:export")
63
64
  nonInteractive: options.nonInteractive,
64
65
  force: options.force,
65
66
  }, true);
67
+ (0, etags_1.saveEtags)(options.rc, projectId, have);
66
68
  });
@@ -19,6 +19,23 @@ function getRulesConfig(projectId, options) {
19
19
  if (dbConfig === undefined) {
20
20
  return [];
21
21
  }
22
+ const rc = options.rc;
23
+ let allDatabases = !options.only;
24
+ const onlyDatabases = new Set();
25
+ if (options.only) {
26
+ const split = options.only.split(",");
27
+ if (split.includes("database")) {
28
+ allDatabases = true;
29
+ }
30
+ else {
31
+ for (const value of split) {
32
+ if (value.startsWith("database:")) {
33
+ const target = value.split(":")[1];
34
+ onlyDatabases.add(target);
35
+ }
36
+ }
37
+ }
38
+ }
22
39
  if (!Array.isArray(dbConfig)) {
23
40
  if (dbConfig && dbConfig.rules) {
24
41
  utils.assertIsStringOrUndefined(options.instance);
@@ -31,22 +48,32 @@ function getRulesConfig(projectId, options) {
31
48
  }
32
49
  }
33
50
  const results = [];
34
- const rc = options.rc;
35
51
  for (const c of dbConfig) {
36
- if (c.target) {
37
- rc.requireTarget(projectId, "database", c.target);
38
- const instances = rc.target(projectId, "database", c.target);
39
- for (const i of instances) {
40
- results.push({ instance: i, rules: c.rules });
52
+ const { instance, target } = c;
53
+ if (target) {
54
+ if (allDatabases || onlyDatabases.has(target)) {
55
+ rc.requireTarget(projectId, "database", target);
56
+ const instances = rc.target(projectId, "database", target);
57
+ for (const i of instances) {
58
+ results.push({ instance: i, rules: c.rules });
59
+ }
60
+ onlyDatabases.delete(target);
41
61
  }
42
62
  }
43
- else if (c.instance) {
44
- results.push(c);
63
+ else if (instance) {
64
+ if (allDatabases) {
65
+ results.push(c);
66
+ }
45
67
  }
46
68
  else {
47
69
  throw new error_1.FirebaseError('Must supply either "target" or "instance" in database config');
48
70
  }
49
71
  }
72
+ if (!allDatabases && onlyDatabases.size !== 0) {
73
+ throw new error_1.FirebaseError(`Could not find configurations in firebase.json for the following database targets: ${[
74
+ ...onlyDatabases,
75
+ ].join(", ")}`);
76
+ }
50
77
  return results;
51
78
  }
52
79
  exports.getRulesConfig = getRulesConfig;
@@ -54,6 +54,7 @@ async function have(projectId) {
54
54
  params: i.config.params,
55
55
  allowedEventTypes: i.config.allowedEventTypes,
56
56
  eventarcChannel: i.config.eventarcChannel,
57
+ etag: i.etag,
57
58
  };
58
59
  if (i.config.extensionRef) {
59
60
  const ref = refs.parse(i.config.extensionRef);
@@ -13,6 +13,7 @@ const extensionsHelper_1 = require("../../extensions/extensionsHelper");
13
13
  const secretsUtils_1 = require("../../extensions/secretsUtils");
14
14
  const secrets_1 = require("./secrets");
15
15
  const warnings_1 = require("../../extensions/warnings");
16
+ const etags_1 = require("../../extensions/etags");
16
17
  async function prepare(context, options, payload) {
17
18
  var _a;
18
19
  const projectId = (0, projectUtils_1.needProjectId)(options);
@@ -28,6 +29,22 @@ async function prepare(context, options, payload) {
28
29
  projectDir: options.config.projectDir,
29
30
  extensions: options.config.get("extensions"),
30
31
  });
32
+ const etagsChanged = (0, etags_1.detectEtagChanges)(options.rc, projectId, context.have);
33
+ if (etagsChanged.length) {
34
+ (0, warnings_1.outOfBandChangesWarning)(etagsChanged);
35
+ if (!options.force && options.nonInteractive) {
36
+ throw new error_1.FirebaseError("Pass the --force flag to overwrite out of band changes in non-interactive mode");
37
+ }
38
+ else if (!options.force &&
39
+ !options.nonInteractive &&
40
+ !(await prompt.promptOnce({
41
+ type: "confirm",
42
+ message: `Do you wish to continue deploying these extension instances?`,
43
+ default: false,
44
+ }))) {
45
+ throw new error_1.FirebaseError("Deployment cancelled");
46
+ }
47
+ }
31
48
  const usingSecrets = await Promise.all((_a = context.want) === null || _a === void 0 ? void 0 : _a.map(secrets_1.checkSpecForSecrets));
32
49
  if (usingSecrets.some((i) => i)) {
33
50
  await (0, secretsUtils_1.ensureSecretManagerApiEnabled)(options);
@@ -44,7 +61,7 @@ async function prepare(context, options, payload) {
44
61
  !options.nonInteractive &&
45
62
  !(await prompt.promptOnce({
46
63
  type: "confirm",
47
- message: `Do you wish to continue deploying these extensions?`,
64
+ message: `Do you wish to continue deploying these extension instances?`,
48
65
  default: true,
49
66
  }))) {
50
67
  throw new error_1.FirebaseError("Deployment cancelled");
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.release = void 0;
4
4
  const queue_1 = require("../../throttler/queue");
5
5
  const tasks = require("./tasks");
6
+ const planner = require("./planner");
6
7
  const error_1 = require("../../error");
7
8
  const errors_1 = require("./errors");
8
9
  const projectUtils_1 = require("../../projectUtils");
10
+ const etags_1 = require("../../extensions/etags");
9
11
  async function release(context, options, payload) {
10
12
  var _a, _b, _c, _d;
11
13
  const projectId = (0, projectUtils_1.needProjectId)(options);
@@ -35,6 +37,8 @@ async function release(context, options, payload) {
35
37
  deploymentQueue.process();
36
38
  deploymentQueue.close();
37
39
  await deploymentPromise;
40
+ const newHave = await planner.have(projectId);
41
+ (0, etags_1.saveEtags)(options.rc, projectId, newHave);
38
42
  if (errorHandler.hasErrors()) {
39
43
  errorHandler.print();
40
44
  throw new error_1.FirebaseError(`Extensions deployment failed.`);
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveBackend = exports.of = exports.empty = void 0;
3
+ exports.toBackend = exports.resolveBackend = exports.of = exports.empty = void 0;
4
4
  const backend = require("./backend");
5
5
  const proto = require("../../gcp/proto");
6
6
  const api = require("../../.../../api");
7
+ const params = require("./params");
8
+ const previews_1 = require("../../previews");
7
9
  const error_1 = require("../../error");
8
10
  const functional_1 = require("../../functional");
9
11
  function empty() {
@@ -20,45 +22,19 @@ function of(endpoints) {
20
22
  return build;
21
23
  }
22
24
  exports.of = of;
23
- function resolveInt(from) {
24
- if (from == null) {
25
- return 0;
26
- }
27
- else if (typeof from === "string") {
28
- throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
29
- }
30
- return from;
31
- }
32
- function resolveString(from) {
33
- if (from == null) {
34
- return "";
35
- }
36
- else if (from.includes("{{") && from.includes("}}")) {
37
- throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
38
- }
39
- return from;
40
- }
41
- function resolveBoolean(from) {
42
- if (from == null) {
43
- return false;
44
- }
45
- else if (typeof from === "string") {
46
- throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
47
- }
48
- return from;
49
- }
50
25
  function isMemoryOption(value) {
51
26
  return value == null || [128, 256, 512, 1024, 2048, 4096, 8192].includes(value);
52
27
  }
53
- function resolveBackend(build, userEnvs) {
54
- for (const param of build.params) {
55
- const expectedEnv = param.param;
56
- if (!userEnvs.hasOwnProperty(expectedEnv)) {
57
- throw new error_1.FirebaseError("Build specified parameter " +
58
- expectedEnv +
59
- " but it was not present in the user dotenv files or Cloud Secret Manager");
60
- }
28
+ async function resolveBackend(build, userEnvOpt, userEnvs) {
29
+ const projectId = userEnvOpt.projectId;
30
+ let paramValues = {};
31
+ if (previews_1.previews.functionsparams) {
32
+ paramValues = await params.resolveParams(build.params, projectId, userEnvs);
61
33
  }
34
+ return toBackend(build, paramValues);
35
+ }
36
+ exports.resolveBackend = resolveBackend;
37
+ function toBackend(build, paramValues) {
62
38
  const bkEndpoints = [];
63
39
  for (const endpointId of Object.keys(build.endpoints)) {
64
40
  const bdEndpoint = build.endpoints[endpointId];
@@ -67,7 +43,7 @@ function resolveBackend(build, userEnvs) {
67
43
  regions = [api.functionsDefaultRegion];
68
44
  }
69
45
  for (const region of regions) {
70
- const trigger = discoverTrigger(bdEndpoint);
46
+ const trigger = discoverTrigger(bdEndpoint, paramValues);
71
47
  if (typeof bdEndpoint.platform === "undefined") {
72
48
  throw new error_1.FirebaseError("platform can't be undefined");
73
49
  }
@@ -76,21 +52,25 @@ function resolveBackend(build, userEnvs) {
76
52
  }
77
53
  let timeout;
78
54
  if (bdEndpoint.timeoutSeconds) {
79
- timeout = resolveInt(bdEndpoint.timeoutSeconds);
55
+ timeout = params.resolveInt(bdEndpoint.timeoutSeconds, paramValues);
80
56
  }
81
57
  else {
82
58
  timeout = 60;
83
59
  }
84
60
  const bkEndpoint = Object.assign({ id: endpointId, project: bdEndpoint.project, region: region, entryPoint: bdEndpoint.entryPoint, platform: bdEndpoint.platform, runtime: bdEndpoint.runtime, timeoutSeconds: timeout }, trigger);
85
- proto.renameIfPresent(bkEndpoint, bdEndpoint, "maxInstances", "maxInstances", resolveInt);
86
- proto.renameIfPresent(bkEndpoint, bdEndpoint, "minInstances", "minInstances", resolveInt);
87
- proto.renameIfPresent(bkEndpoint, bdEndpoint, "concurrency", "concurrency", resolveInt);
61
+ proto.renameIfPresent(bkEndpoint, bdEndpoint, "maxInstances", "maxInstances", (from) => {
62
+ return params.resolveInt(from, paramValues);
63
+ });
64
+ proto.renameIfPresent(bkEndpoint, bdEndpoint, "minInstances", "minInstances", (from) => {
65
+ return params.resolveInt(from, paramValues);
66
+ });
67
+ proto.renameIfPresent(bkEndpoint, bdEndpoint, "concurrency", "concurrency", (from) => {
68
+ return params.resolveInt(from, paramValues);
69
+ });
88
70
  proto.copyIfPresent(bkEndpoint, bdEndpoint, "ingressSettings", "availableMemoryMb", "environmentVariables", "labels");
89
71
  proto.copyIfPresent(bkEndpoint, bdEndpoint, "secretEnvironmentVariables");
90
72
  if (bdEndpoint.vpc) {
91
- bkEndpoint.vpc = {
92
- connector: resolveString(bdEndpoint.vpc.connector).replace("$REGION", region),
93
- };
73
+ bkEndpoint.vpc = { connector: params.resolveString(bdEndpoint.vpc.connector, paramValues) };
94
74
  proto.copyIfPresent(bkEndpoint.vpc, bdEndpoint.vpc, "egressSettings");
95
75
  }
96
76
  proto.renameIfPresent(bkEndpoint, bdEndpoint, "serviceAccountEmail", "serviceAccount");
@@ -104,13 +84,16 @@ function resolveBackend(build, userEnvs) {
104
84
  bkend.requiredAPIs = build.requiredAPIs;
105
85
  return bkend;
106
86
  }
107
- exports.resolveBackend = resolveBackend;
108
- function discoverTrigger(endpoint) {
87
+ exports.toBackend = toBackend;
88
+ function discoverTrigger(endpoint, paramValues) {
89
+ const resolveInt = (from) => params.resolveInt(from, paramValues);
90
+ const resolveString = (from) => params.resolveString(from, paramValues);
91
+ const resolveBoolean = (from) => params.resolveBoolean(from, paramValues);
109
92
  let trigger;
110
93
  if ("httpsTrigger" in endpoint) {
111
94
  const bkHttps = {};
112
95
  if (endpoint.httpsTrigger.invoker) {
113
- bkHttps.invoker = [endpoint.httpsTrigger.invoker];
96
+ bkHttps.invoker = endpoint.httpsTrigger.invoker;
114
97
  }
115
98
  trigger = { httpsTrigger: bkHttps };
116
99
  }
@@ -122,22 +105,30 @@ function discoverTrigger(endpoint) {
122
105
  }
123
106
  else if ("eventTrigger" in endpoint) {
124
107
  const bkEventFilters = {};
125
- for (const key in endpoint.eventTrigger.eventFilters) {
126
- if (typeof key === "string") {
127
- bkEventFilters[key] = resolveString(endpoint.eventTrigger.eventFilters[key]);
128
- }
108
+ for (const [key, value] of Object.entries(endpoint.eventTrigger.eventFilters)) {
109
+ bkEventFilters[key] = params.resolveString(value, paramValues);
129
110
  }
130
111
  const bkEvent = {
131
112
  eventType: endpoint.eventTrigger.eventType,
132
113
  eventFilters: bkEventFilters,
133
- retry: resolveBoolean(endpoint.eventTrigger.retry),
114
+ retry: resolveBoolean(endpoint.eventTrigger.retry || false),
134
115
  };
116
+ if (endpoint.eventTrigger.eventFilterPathPatterns) {
117
+ const bkEventFiltersPathPatterns = {};
118
+ for (const [key, value] of Object.entries(endpoint.eventTrigger.eventFilterPathPatterns)) {
119
+ bkEventFiltersPathPatterns[key] = params.resolveString(value, paramValues);
120
+ }
121
+ bkEvent.eventFilterPathPatterns = bkEventFiltersPathPatterns;
122
+ }
135
123
  if (endpoint.eventTrigger.serviceAccount) {
136
124
  bkEvent.serviceAccountEmail = endpoint.eventTrigger.serviceAccount;
137
125
  }
138
126
  if (endpoint.eventTrigger.region) {
139
127
  bkEvent.region = resolveString(endpoint.eventTrigger.region);
140
128
  }
129
+ if (endpoint.eventTrigger.channel) {
130
+ bkEvent.channel = endpoint.eventTrigger.channel;
131
+ }
141
132
  trigger = { eventTrigger: bkEvent };
142
133
  }
143
134
  else if ("scheduleTrigger" in endpoint) {
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveParams = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
4
+ const logger_1 = require("../../logger");
5
+ const error_1 = require("../../error");
6
+ const prompt_1 = require("../../prompt");
7
+ const functional_1 = require("../../functional");
8
+ function isCEL(expr) {
9
+ return typeof expr === "string" && expr.includes("{{") && expr.includes("}}");
10
+ }
11
+ function dependenciesCEL(expr) {
12
+ const deps = [];
13
+ const paramCapture = /{{ params\.(\w+) }}/g;
14
+ let match;
15
+ while ((match = paramCapture.exec(expr)) != null) {
16
+ deps.push(match[1]);
17
+ }
18
+ return deps;
19
+ }
20
+ function resolveInt(from, paramValues) {
21
+ if (typeof from === "number") {
22
+ return from;
23
+ }
24
+ const match = /\A{{ params\.(\w+) }}\z/.exec(from);
25
+ if (!match) {
26
+ throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
27
+ }
28
+ const referencedParamValue = paramValues[match[1]];
29
+ if (typeof referencedParamValue !== "number") {
30
+ throw new error_1.FirebaseError("Referenced numeric parameter '" +
31
+ match +
32
+ "' resolved to non-number value " +
33
+ referencedParamValue);
34
+ }
35
+ return referencedParamValue;
36
+ }
37
+ exports.resolveInt = resolveInt;
38
+ function resolveString(from, paramValues) {
39
+ if (!isCEL(from)) {
40
+ return from;
41
+ }
42
+ let output = from;
43
+ const paramCapture = /{{ params\.(\w+) }}/g;
44
+ let match;
45
+ while ((match = paramCapture.exec(from)) != null) {
46
+ const referencedParamValue = paramValues[match[1]];
47
+ if (typeof referencedParamValue !== "string") {
48
+ throw new error_1.FirebaseError("Referenced string parameter '" +
49
+ match[1] +
50
+ "' resolved to non-string value " +
51
+ referencedParamValue);
52
+ }
53
+ output = output.replace(`{{ params.${match[1]} }}`, referencedParamValue);
54
+ }
55
+ if (isCEL(output)) {
56
+ throw new error_1.FirebaseError("CEL evaluation of non-identity expression '" + from + "' not yet supported");
57
+ }
58
+ return output;
59
+ }
60
+ exports.resolveString = resolveString;
61
+ function resolveBoolean(from, paramValues) {
62
+ if (typeof from === "string" && /{{ params\.(\w+) }}/.test(from)) {
63
+ const match = /{{ params\.(\w+) }}/.exec(from);
64
+ const referencedParamValue = paramValues[match[1]];
65
+ if (typeof referencedParamValue !== "boolean") {
66
+ throw new error_1.FirebaseError("Referenced boolean parameter '" +
67
+ match +
68
+ "' resolved to non-boolean value " +
69
+ referencedParamValue);
70
+ }
71
+ return referencedParamValue;
72
+ }
73
+ else if (typeof from === "string") {
74
+ throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
75
+ }
76
+ return from;
77
+ }
78
+ exports.resolveBoolean = resolveBoolean;
79
+ function resolveDefaultCEL(type, expr, currentEnv) {
80
+ const deps = dependenciesCEL(expr);
81
+ const allDepsFound = deps.every((dep) => !!currentEnv[dep]);
82
+ if (!allDepsFound) {
83
+ throw new error_1.FirebaseError("Build specified parameter with un-resolvable default value " +
84
+ expr +
85
+ "; dependencies missing.");
86
+ }
87
+ switch (type) {
88
+ case "string":
89
+ return resolveString(expr, currentEnv);
90
+ case "int":
91
+ return resolveInt(expr, currentEnv);
92
+ default:
93
+ throw new error_1.FirebaseError("Build specified parameter with default " + expr + " of unsupported type");
94
+ }
95
+ }
96
+ function canSatisfyParam(param, value) {
97
+ if (param.type === "string") {
98
+ return typeof value === "string";
99
+ }
100
+ else if (param.type === "int") {
101
+ return typeof value === "number" && Number.isInteger(value);
102
+ }
103
+ (0, functional_1.assertExhaustive)(param);
104
+ }
105
+ async function resolveParams(params, projectId, userEnvs) {
106
+ const paramValues = {};
107
+ for (const param of params.filter((param) => userEnvs.hasOwnProperty(param.param))) {
108
+ if (!canSatisfyParam(param, userEnvs[param.param])) {
109
+ throw new error_1.FirebaseError("Parameter " +
110
+ param.param +
111
+ " resolved to value from dotenv files " +
112
+ userEnvs[param.param] +
113
+ " of wrong type");
114
+ }
115
+ paramValues[param.param] = userEnvs[param.param];
116
+ }
117
+ for (const param of params.filter((param) => !userEnvs.hasOwnProperty(param.param))) {
118
+ let paramDefault = param.default;
119
+ if (paramDefault && isCEL(paramDefault)) {
120
+ paramDefault = resolveDefaultCEL(param.type, paramDefault, paramValues);
121
+ }
122
+ if (paramDefault && !canSatisfyParam(param, paramDefault)) {
123
+ throw new error_1.FirebaseError("Parameter " + param.param + " has default value " + paramDefault + " of wrong type");
124
+ }
125
+ paramValues[param.param] = await promptParam(param, paramDefault);
126
+ }
127
+ return paramValues;
128
+ }
129
+ exports.resolveParams = resolveParams;
130
+ async function promptParam(param, resolvedDefault) {
131
+ if (param.type === "string") {
132
+ return promptStringParam(param, resolvedDefault);
133
+ }
134
+ else if (param.type === "int") {
135
+ return promptIntParam(param, resolvedDefault);
136
+ }
137
+ (0, functional_1.assertExhaustive)(param);
138
+ }
139
+ async function promptStringParam(param, resolvedDefault) {
140
+ if (!param.input) {
141
+ const defaultToText = { type: "text", text: {} };
142
+ param.input = defaultToText;
143
+ }
144
+ switch (param.input.type) {
145
+ case "select":
146
+ throw new error_1.FirebaseError("Build specified string parameter " + param.param + " with unsupported input type 'select'");
147
+ case "text":
148
+ default:
149
+ let prompt = `Enter a value for ${param.label || param.param}`;
150
+ if (param.description) {
151
+ prompt += ` \n(${param.description})`;
152
+ }
153
+ return await (0, prompt_1.promptOnce)({
154
+ name: param.param,
155
+ type: "input",
156
+ default: resolvedDefault,
157
+ message: prompt,
158
+ });
159
+ }
160
+ }
161
+ async function promptIntParam(param, resolvedDefault) {
162
+ if (!param.input) {
163
+ const defaultToText = { type: "text", text: {} };
164
+ param.input = defaultToText;
165
+ }
166
+ switch (param.input.type) {
167
+ case "select":
168
+ throw new error_1.FirebaseError("Build specified int parameter " + param.param + " with unsupported input type 'select'");
169
+ case "text":
170
+ default:
171
+ let prompt = `Enter a value for ${param.label || param.param}`;
172
+ if (param.description) {
173
+ prompt += ` \n(${param.description})`;
174
+ }
175
+ let res;
176
+ while (true) {
177
+ res = await (0, prompt_1.promptOnce)({
178
+ name: param.param,
179
+ type: "number",
180
+ default: resolvedDefault,
181
+ message: prompt,
182
+ });
183
+ if (Number.isInteger(res)) {
184
+ return res;
185
+ }
186
+ logger_1.logger.error(`${param.label || param.param} must be an integer; retrying...`);
187
+ }
188
+ }
189
+ }
@@ -78,7 +78,7 @@ async function prepare(context, options, payload) {
78
78
  const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
79
79
  const envs = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
80
80
  const wantBuild = await runtimeDelegate.discoverBuild(runtimeConfig, firebaseEnvs);
81
- const wantBackend = build.resolveBackend(wantBuild, userEnvs);
81
+ const wantBackend = await build.resolveBackend(wantBuild, userEnvOpt, userEnvs);
82
82
  wantBackend.environmentVariables = envs;
83
83
  for (const endpoint of backend.allEndpoints(wantBackend)) {
84
84
  endpoint.environmentVariables = wantBackend.environmentVariables;
@@ -67,6 +67,10 @@ async function release(context, options, payload) {
67
67
  const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
68
68
  if (allErrors.length) {
69
69
  const opts = allErrors.length === 1 ? { original: allErrors[0] } : { children: allErrors };
70
+ logger_1.logger.debug("Functions deploy failed.");
71
+ for (const error of allErrors) {
72
+ logger_1.logger.debug(JSON.stringify(error, null, 2));
73
+ }
70
74
  throw new error_1.FirebaseError("There was an error deploying functions", Object.assign(Object.assign({}, opts), { exit: 2 }));
71
75
  }
72
76
  }
@@ -28,10 +28,12 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
28
28
  (0, parsing_1.requireKeys)("", manifest, "endpoints");
29
29
  (0, parsing_1.assertKeyTypes)("", manifest, {
30
30
  specVersion: "string",
31
+ params: "array",
31
32
  requiredAPIs: "array",
32
33
  endpoints: "object",
33
34
  });
34
35
  const bd = build.empty();
36
+ bd.params = manifest.params || [];
35
37
  bd.requiredAPIs = parseRequiredAPIs(manifest);
36
38
  for (const id of Object.keys(manifest.endpoints)) {
37
39
  const me = manifest.endpoints[id];
@@ -49,6 +51,7 @@ function backendFromV1Alpha1(yaml, project, region, runtime) {
49
51
  (0, parsing_1.requireKeys)("", manifest, "endpoints");
50
52
  (0, parsing_1.assertKeyTypes)("", manifest, {
51
53
  specVersion: "string",
54
+ params: "array",
52
55
  requiredAPIs: "array",
53
56
  endpoints: "object",
54
57
  });
@@ -126,7 +126,7 @@ function addResourcesToBuild(projectId, runtime, annotation, want) {
126
126
  logger_1.logger.warn(`Ignoring retry policy for HTTPS function ${annotation.name}`);
127
127
  }
128
128
  if (annotation.httpsTrigger.invoker) {
129
- trigger.invoker = annotation.httpsTrigger.invoker[0];
129
+ trigger.invoker = annotation.httpsTrigger.invoker;
130
130
  }
131
131
  triggered = { httpsTrigger: trigger };
132
132
  }