firebase-tools 11.18.0 → 11.20.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 (35) hide show
  1. package/lib/api.js +3 -2
  2. package/lib/bin/firebase.js +0 -0
  3. package/lib/commands/functions-delete.js +1 -1
  4. package/lib/commands/index.js +5 -0
  5. package/lib/commands/internaltesting-functions-discover.js +25 -0
  6. package/lib/deploy/extensions/prepare.js +6 -1
  7. package/lib/deploy/extensions/v2FunctionHelper.js +53 -0
  8. package/lib/deploy/functions/build.js +17 -2
  9. package/lib/deploy/functions/cel.js +90 -13
  10. package/lib/deploy/functions/params.js +141 -6
  11. package/lib/deploy/functions/prepare.js +40 -25
  12. package/lib/deploy/functions/release/fabricator.js +27 -1
  13. package/lib/deploy/functions/runtimes/discovery/parsing.js +7 -1
  14. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +1 -1
  15. package/lib/deploy/functions/runtimes/node/index.js +2 -2
  16. package/lib/deploy/functions/runtimes/node/versioning.js +30 -14
  17. package/lib/emulator/auth/operations.js +1 -1
  18. package/lib/emulator/commandUtils.js +2 -1
  19. package/lib/emulator/downloadableEmulators.js +6 -6
  20. package/lib/emulator/env.js +29 -27
  21. package/lib/emulator/extensionsEmulator.js +14 -9
  22. package/lib/emulator/functionsEmulator.js +16 -8
  23. package/lib/emulator/pubsubEmulator.js +13 -1
  24. package/lib/emulator/storage/rules/runtime.js +2 -2
  25. package/lib/emulator/workQueue.js +11 -6
  26. package/lib/experiments.js +6 -0
  27. package/lib/extensions/emulator/triggerHelper.js +12 -2
  28. package/lib/frameworks/index.js +7 -1
  29. package/lib/frameworks/next/constants.js +10 -0
  30. package/lib/frameworks/next/index.js +146 -146
  31. package/lib/frameworks/next/utils.js +65 -7
  32. package/lib/gcp/eventarc.js +42 -0
  33. package/lib/serve/hosting.js +5 -5
  34. package/npm-shrinkwrap.json +463 -737
  35. package/package.json +2 -5
package/lib/api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getScopes = exports.githubClientSecret = exports.githubClientId = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.deployOrigin = exports.consoleOrigin = exports.authOrigin = exports.appengineOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
4
- exports.setScopes = void 0;
3
+ exports.githubClientSecret = exports.githubClientId = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.deployOrigin = exports.consoleOrigin = exports.authOrigin = exports.appengineOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
4
+ exports.setScopes = exports.getScopes = void 0;
5
5
  const constants_1 = require("./emulator/constants");
6
6
  const logger_1 = require("./logger");
7
7
  const scopes = require("./scopes");
@@ -22,6 +22,7 @@ exports.consoleOrigin = utils.envOverride("FIREBASE_CONSOLE_URL", "https://conso
22
22
  exports.deployOrigin = utils.envOverride("FIREBASE_DEPLOY_URL", utils.envOverride("FIREBASE_UPLOAD_URL", "https://deploy.firebase.com"));
23
23
  exports.dynamicLinksOrigin = utils.envOverride("FIREBASE_DYNAMIC_LINKS_URL", "https://firebasedynamiclinks.googleapis.com");
24
24
  exports.dynamicLinksKey = utils.envOverride("FIREBASE_DYNAMIC_LINKS_KEY", "AIzaSyB6PtY5vuiSB8MNgt20mQffkOlunZnHYiQ");
25
+ exports.eventarcOrigin = utils.envOverride("EVENTARC_URL", "https://eventarc.googleapis.com");
25
26
  exports.firebaseApiOrigin = utils.envOverride("FIREBASE_API_URL", "https://firebase.googleapis.com");
26
27
  exports.firebaseExtensionsRegistryOrigin = utils.envOverride("FIREBASE_EXT_REGISTRY_ORIGIN", "https://extensions-registry.firebaseapp.com");
27
28
  exports.firedataOrigin = utils.envOverride("FIREBASE_FIREDATA_URL", "https://mobilesdk-pa.googleapis.com");
File without changes
@@ -30,7 +30,7 @@ exports.command = new command_1.Command("functions:delete [filters...]")
30
30
  }
31
31
  const context = {
32
32
  projectId: (0, projectUtils_1.needProjectId)(options),
33
- filters: filters.map((f) => ({ idChunks: f.split(".") })),
33
+ filters: filters.map((f) => ({ idChunks: f.split(/[-.]/) })),
34
34
  };
35
35
  const [config, existingBackend] = await Promise.all([
36
36
  functionsConfig.getFirebaseConfig(options),
@@ -137,6 +137,11 @@ function load(client) {
137
137
  client.hosting.sites.get = loadCommand("hosting-sites-get");
138
138
  client.hosting.sites.list = loadCommand("hosting-sites-list");
139
139
  client.init = loadCommand("init");
140
+ if (experiments.isEnabled("internaltesting")) {
141
+ client.internaltesting = {};
142
+ client.internaltesting.functions = {};
143
+ client.internaltesting.functions.discover = loadCommand("internaltesting-functions-discover");
144
+ }
140
145
  client.login = loadCommand("login");
141
146
  client.login.add = loadCommand("login-add");
142
147
  client.login.ci = loadCommand("login-ci");
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const logger_1 = require("../logger");
6
+ const prepare_1 = require("../deploy/functions/prepare");
7
+ const projectConfig_1 = require("../functions/projectConfig");
8
+ const adminSdkConfig_1 = require("../emulator/adminSdkConfig");
9
+ const projectUtils_1 = require("../projectUtils");
10
+ const error_1 = require("../error");
11
+ exports.command = new command_1.Command("internaltesting:functions:discover")
12
+ .description("discover function triggers defined in the current project directory")
13
+ .action(async (options) => {
14
+ const projectId = (0, projectUtils_1.needProjectId)(options);
15
+ const fnConfig = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
16
+ const firebaseConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(projectId);
17
+ if (!firebaseConfig) {
18
+ throw new error_1.FirebaseError("Admin SDK config unexpectedly undefined - have you run firebase init?");
19
+ }
20
+ const builds = await (0, prepare_1.loadCodebases)(fnConfig, options, firebaseConfig, {
21
+ firebase: firebaseConfig,
22
+ });
23
+ logger_1.logger.info(JSON.stringify(builds, null, 2));
24
+ return builds;
25
+ });
@@ -14,8 +14,9 @@ const secretsUtils_1 = require("../../extensions/secretsUtils");
14
14
  const secrets_1 = require("./secrets");
15
15
  const warnings_1 = require("../../extensions/warnings");
16
16
  const etags_1 = require("../../extensions/etags");
17
+ const v2FunctionHelper_1 = require("./v2FunctionHelper");
17
18
  async function prepare(context, options, payload) {
18
- var _a;
19
+ var _a, _b;
19
20
  const projectId = (0, projectUtils_1.needProjectId)(options);
20
21
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
21
22
  const aliases = (0, projectUtils_1.getAliases)(options, projectId);
@@ -49,6 +50,10 @@ async function prepare(context, options, payload) {
49
50
  if (usingSecrets.some((i) => i)) {
50
51
  await (0, secretsUtils_1.ensureSecretManagerApiEnabled)(options);
51
52
  }
53
+ const usingV2Functions = await Promise.all((_b = context.want) === null || _b === void 0 ? void 0 : _b.map(v2FunctionHelper_1.checkSpecForV2Functions));
54
+ if (usingV2Functions) {
55
+ await (0, v2FunctionHelper_1.ensureNecessaryV2ApisAndRoles)(options);
56
+ }
52
57
  payload.instancesToCreate = context.want.filter((i) => { var _a; return !((_a = context.have) === null || _a === void 0 ? void 0 : _a.some(matchesInstanceId(i))); });
53
58
  payload.instancesToConfigure = context.want.filter((i) => { var _a; return (_a = context.have) === null || _a === void 0 ? void 0 : _a.some(isConfigure(i)); });
54
59
  payload.instancesToUpdate = context.want.filter((i) => { var _a; return (_a = context.have) === null || _a === void 0 ? void 0 : _a.some(isUpdate(i)); });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureNecessaryV2ApisAndRoles = exports.checkSpecForV2Functions = void 0;
4
+ const getProjectNumber_1 = require("../../getProjectNumber");
5
+ const resourceManager = require("../../gcp/resourceManager");
6
+ const logger_1 = require("../../logger");
7
+ const error_1 = require("../../error");
8
+ const ensureApiEnabled_1 = require("../../ensureApiEnabled");
9
+ const planner = require("./planner");
10
+ const projectUtils_1 = require("../../projectUtils");
11
+ const SERVICE_AGENT_ROLE = "roles/eventarc.eventReceiver";
12
+ async function checkSpecForV2Functions(i) {
13
+ const extensionSpec = await planner.getExtensionSpec(i);
14
+ return extensionSpec.resources.some((r) => r.type === "firebaseextensions.v1beta.v2function");
15
+ }
16
+ exports.checkSpecForV2Functions = checkSpecForV2Functions;
17
+ async function ensureNecessaryV2ApisAndRoles(options) {
18
+ const projectId = (0, projectUtils_1.needProjectId)(options);
19
+ await (0, ensureApiEnabled_1.ensure)(projectId, "compute.googleapis.com", "extensions", options.markdown);
20
+ await ensureComputeP4SARole(projectId);
21
+ }
22
+ exports.ensureNecessaryV2ApisAndRoles = ensureNecessaryV2ApisAndRoles;
23
+ async function ensureComputeP4SARole(projectId) {
24
+ const projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ projectId });
25
+ const saEmail = `${projectNumber}-compute@developer.gserviceaccount.com`;
26
+ let policy;
27
+ try {
28
+ policy = await resourceManager.getIamPolicy(projectId);
29
+ }
30
+ catch (e) {
31
+ if (e instanceof error_1.FirebaseError && e.status === 403) {
32
+ throw new error_1.FirebaseError("Unable to get project IAM policy, permission denied (403). Please " +
33
+ "make sure you have sufficient project privileges or if this is a brand new project " +
34
+ "try again in a few minutes.");
35
+ }
36
+ throw e;
37
+ }
38
+ if (policy.bindings.find((b) => b.role === SERVICE_AGENT_ROLE && b.members.includes("serviceAccount:" + saEmail))) {
39
+ logger_1.logger.debug("Compute Service API Agent IAM policy OK");
40
+ return true;
41
+ }
42
+ else {
43
+ logger_1.logger.debug("Firebase Extensions Service Agent is missing a required IAM role " +
44
+ "`Firebase Extensions API Service Agent`.");
45
+ policy.bindings.push({
46
+ role: SERVICE_AGENT_ROLE,
47
+ members: ["serviceAccount:" + saEmail],
48
+ });
49
+ await resourceManager.setIamPolicy(projectId, policy, "bindings");
50
+ logger_1.logger.debug("Compute Service API Agent IAM policy updated successfully");
51
+ return true;
52
+ }
53
+ }
@@ -84,6 +84,7 @@ function envWithTypes(definedParams, rawEnvs) {
84
84
  string: true,
85
85
  boolean: true,
86
86
  number: true,
87
+ list: true,
87
88
  };
88
89
  for (const param of definedParams) {
89
90
  if (param.name === envName) {
@@ -92,6 +93,7 @@ function envWithTypes(definedParams, rawEnvs) {
92
93
  string: true,
93
94
  boolean: false,
94
95
  number: false,
96
+ list: false,
95
97
  };
96
98
  }
97
99
  else if (param.type === "int") {
@@ -99,6 +101,7 @@ function envWithTypes(definedParams, rawEnvs) {
99
101
  string: false,
100
102
  boolean: false,
101
103
  number: true,
104
+ list: false,
102
105
  };
103
106
  }
104
107
  else if (param.type === "boolean") {
@@ -106,6 +109,15 @@ function envWithTypes(definedParams, rawEnvs) {
106
109
  string: false,
107
110
  boolean: true,
108
111
  number: false,
112
+ list: false,
113
+ };
114
+ }
115
+ else if (param.type === "list") {
116
+ providedType = {
117
+ string: false,
118
+ boolean: false,
119
+ number: false,
120
+ list: true,
109
121
  };
110
122
  }
111
123
  }
@@ -163,10 +175,13 @@ function toBackend(build, paramValues) {
163
175
  if (r.resolveBoolean(bdEndpoint.omit || false)) {
164
176
  continue;
165
177
  }
166
- let regions = bdEndpoint.region;
167
- if (typeof regions === "undefined") {
178
+ let regions = [];
179
+ if (!bdEndpoint.region) {
168
180
  regions = [api.functionsDefaultRegion];
169
181
  }
182
+ else {
183
+ regions = params.resolveList(bdEndpoint.region, paramValues);
184
+ }
170
185
  for (const region of regions) {
171
186
  const trigger = discoverTrigger(bdEndpoint, region, r);
172
187
  if (typeof bdEndpoint.platform === "undefined") {
@@ -11,6 +11,9 @@ const comparisonRegexp = new RegExp(/{{ params\.(\S+) CMP (.+) }}/.source.replac
11
11
  const dualTernaryRegexp = new RegExp(/{{ params\.(\S+) CMP params\.(\S+) \? (.+) : (.+) }/.source.replace("CMP", CMP));
12
12
  const ternaryRegexp = new RegExp(/{{ params\.(\S+) CMP (.+) \? (.+) : (.+) }/.source.replace("CMP", CMP));
13
13
  const literalTernaryRegexp = /{{ params\.(\S+) \? (.+) : (.+) }/;
14
+ function listEquals(a, b) {
15
+ return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
16
+ }
14
17
  function isCelExpression(value) {
15
18
  return typeof value === "string" && value.includes("{{") && value.includes("}}");
16
19
  }
@@ -37,6 +40,7 @@ class ExprParseError extends error_1.FirebaseError {
37
40
  }
38
41
  exports.ExprParseError = ExprParseError;
39
42
  function resolveExpression(wantType, expr, params) {
43
+ expr = preprocessLists(wantType, expr, params);
40
44
  if (isIdentityExpression(expr)) {
41
45
  return resolveIdentity(wantType, expr, params);
42
46
  }
@@ -60,10 +64,52 @@ function resolveExpression(wantType, expr, params) {
60
64
  }
61
65
  }
62
66
  exports.resolveExpression = resolveExpression;
67
+ function preprocessLists(wantType, expr, params) {
68
+ let rv = expr;
69
+ const listMatcher = /\[[^\[\]]*\]/g;
70
+ let match;
71
+ while ((match = listMatcher.exec(expr)) != null) {
72
+ const list = match[0];
73
+ const resolved = resolveList("string", list, params);
74
+ rv = rv.replace(list, JSON.stringify(resolved));
75
+ }
76
+ return rv;
77
+ }
78
+ function resolveList(wantType, list, params) {
79
+ if (!list.startsWith("[") || !list.endsWith("]")) {
80
+ throw new ExprParseError("Invalid list: must start with '[' and end with ']'");
81
+ }
82
+ else if (list === "[]") {
83
+ return [];
84
+ }
85
+ const rv = [];
86
+ const entries = list.slice(1, -1).split(",");
87
+ for (const entry of entries) {
88
+ const trimmed = entry.trim();
89
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
90
+ rv.push(trimmed.slice(1, -1));
91
+ }
92
+ else if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
93
+ rv.push(resolveExpression("string", trimmed, params));
94
+ }
95
+ else {
96
+ const paramMatch = paramRegexp.exec(trimmed);
97
+ if (!paramMatch) {
98
+ throw new ExprParseError(`Malformed list component ${trimmed}`);
99
+ }
100
+ else if (!(paramMatch[1] in params)) {
101
+ throw new ExprParseError(`List expansion referenced nonexistent param ${paramMatch[1]}`);
102
+ }
103
+ rv.push(resolveParamListOrLiteral("string", trimmed, params));
104
+ }
105
+ }
106
+ return rv;
107
+ }
63
108
  function assertType(wantType, paramName, paramValue) {
64
109
  if ((wantType === "string" && !paramValue.legalString) ||
65
110
  (wantType === "number" && !paramValue.legalNumber) ||
66
- (wantType === "boolean" && !paramValue.legalBoolean)) {
111
+ (wantType === "boolean" && !paramValue.legalBoolean) ||
112
+ (wantType === "string[]" && !paramValue.legalList)) {
67
113
  throw new ExprParseError(`Illegal type coercion of param ${paramName} to type ${wantType}`);
68
114
  }
69
115
  }
@@ -78,6 +124,9 @@ function readParamValue(wantType, paramName, paramValue) {
78
124
  else if (wantType === "boolean") {
79
125
  return paramValue.asBoolean();
80
126
  }
127
+ else if (wantType === "string[]") {
128
+ return paramValue.asList();
129
+ }
81
130
  else {
82
131
  (0, functional_1.assertExhaustive)(wantType);
83
132
  }
@@ -103,9 +152,9 @@ function resolveComparison(expr, params) {
103
152
  const test = function (a, b) {
104
153
  switch (cmp) {
105
154
  case "!=":
106
- return a !== b;
155
+ return Array.isArray(a) ? !listEquals(a, b) : a !== b;
107
156
  case "==":
108
- return a === b;
157
+ return Array.isArray(a) ? listEquals(a, b) : a === b;
109
158
  case ">=":
110
159
  return a >= b;
111
160
  case "<=":
@@ -136,6 +185,13 @@ function resolveComparison(expr, params) {
136
185
  rhs = resolveLiteral("boolean", match[3]);
137
186
  return test(lhsVal.asBoolean(), rhs);
138
187
  }
188
+ else if (lhsVal.legalList) {
189
+ if (!["==", "!="].includes(cmp)) {
190
+ throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`);
191
+ }
192
+ rhs = resolveLiteral("string[]", match[3]);
193
+ return test(lhsVal.asList(), rhs);
194
+ }
139
195
  else {
140
196
  throw new ExprParseError(`Could not infer type of param ${lhsName} used in comparison operation`);
141
197
  }
@@ -149,9 +205,9 @@ function resolveDualComparison(expr, params) {
149
205
  const test = function (a, b) {
150
206
  switch (cmp) {
151
207
  case "!=":
152
- return a !== b;
208
+ return Array.isArray(a) ? !listEquals(a, b) : a !== b;
153
209
  case "==":
154
- return a === b;
210
+ return Array.isArray(a) ? listEquals(a, b) : a === b;
155
211
  case ">=":
156
212
  return a >= b;
157
213
  case "<=":
@@ -192,6 +248,15 @@ function resolveDualComparison(expr, params) {
192
248
  }
193
249
  return test(lhsVal.asBoolean(), rhsVal.asBoolean());
194
250
  }
251
+ else if (lhsVal.legalList) {
252
+ if (!rhsVal.legalList) {
253
+ throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`);
254
+ }
255
+ if (!["==", "!="].includes(cmp)) {
256
+ throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`);
257
+ }
258
+ return test(lhsVal.asList(), rhsVal.asList());
259
+ }
195
260
  else {
196
261
  throw new ExprParseError(`could not infer type of param ${lhsName} used in comparison operation`);
197
262
  }
@@ -204,10 +269,10 @@ function resolveTernary(wantType, expr, params) {
204
269
  const comparisonExpr = `{{ params.${match[1]} ${match[2]} ${match[3]} }}`;
205
270
  const isTrue = resolveComparison(comparisonExpr, params);
206
271
  if (isTrue) {
207
- return resolveParamOrLiteral(wantType, match[4], params);
272
+ return resolveParamListOrLiteral(wantType, match[4], params);
208
273
  }
209
274
  else {
210
- return resolveParamOrLiteral(wantType, match[5], params);
275
+ return resolveParamListOrLiteral(wantType, match[5], params);
211
276
  }
212
277
  }
213
278
  function resolveDualTernary(wantType, expr, params) {
@@ -218,10 +283,10 @@ function resolveDualTernary(wantType, expr, params) {
218
283
  const comparisonExpr = `{{ params.${match[1]} ${match[2]} params.${match[3]} }}`;
219
284
  const isTrue = resolveDualComparison(comparisonExpr, params);
220
285
  if (isTrue) {
221
- return resolveParamOrLiteral(wantType, match[4], params);
286
+ return resolveParamListOrLiteral(wantType, match[4], params);
222
287
  }
223
288
  else {
224
- return resolveParamOrLiteral(wantType, match[5], params);
289
+ return resolveParamListOrLiteral(wantType, match[5], params);
225
290
  }
226
291
  }
227
292
  function resolveLiteralTernary(wantType, expr, params) {
@@ -238,13 +303,13 @@ function resolveLiteralTernary(wantType, expr, params) {
238
303
  throw new ExprParseError("CEL ternary expression '" + expr + "' is conditional on non-boolean param " + paramName);
239
304
  }
240
305
  if (paramValue.asBoolean()) {
241
- return resolveParamOrLiteral(wantType, match[2], params);
306
+ return resolveParamListOrLiteral(wantType, match[2], params);
242
307
  }
243
308
  else {
244
- return resolveParamOrLiteral(wantType, match[3], params);
309
+ return resolveParamListOrLiteral(wantType, match[3], params);
245
310
  }
246
311
  }
247
- function resolveParamOrLiteral(wantType, field, params) {
312
+ function resolveParamListOrLiteral(wantType, field, params) {
248
313
  const match = paramRegexp.exec(field);
249
314
  if (!match) {
250
315
  return resolveLiteral(wantType, field);
@@ -259,7 +324,19 @@ function resolveLiteral(wantType, value) {
259
324
  if (paramRegexp.exec(value)) {
260
325
  throw new ExprParseError("CEL tried to evaluate param." + value + " in a context which only permits literal values");
261
326
  }
262
- if (wantType === "number") {
327
+ if (wantType === "string[]") {
328
+ const parsed = JSON.parse(value);
329
+ if (!Array.isArray(parsed)) {
330
+ throw new ExprParseError(`CEL tried to read non-list ${JSON.stringify(parsed)} as a list`);
331
+ }
332
+ for (const shouldBeString of parsed) {
333
+ if (typeof shouldBeString !== "string") {
334
+ throw new ExprParseError(`Evaluated CEL list ${JSON.stringify(parsed)} contained non-string values`);
335
+ }
336
+ }
337
+ return parsed;
338
+ }
339
+ else if (wantType === "number") {
263
340
  if (isNaN(+value)) {
264
341
  throw new ExprParseError("CEL literal " + value + " does not seem to be a number");
265
342
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveParams = exports.ParamValue = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
3
+ exports.resolveParams = exports.ParamValue = exports.isMultiSelectInput = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveList = exports.resolveString = exports.resolveInt = void 0;
4
4
  const logger_1 = require("../../logger");
5
5
  const error_1 = require("../../error");
6
6
  const prompt_1 = require("../../prompt");
@@ -38,6 +38,21 @@ function resolveString(from, paramValues) {
38
38
  return output;
39
39
  }
40
40
  exports.resolveString = resolveString;
41
+ function resolveList(from, paramValues) {
42
+ if (!from) {
43
+ return [];
44
+ }
45
+ else if (Array.isArray(from)) {
46
+ return from.map((entry) => resolveString(entry, paramValues));
47
+ }
48
+ else if (typeof from === "string") {
49
+ return (0, cel_1.resolveExpression)("string[]", from, paramValues);
50
+ }
51
+ else {
52
+ (0, functional_1.assertExhaustive)(from);
53
+ }
54
+ }
55
+ exports.resolveList = resolveList;
41
56
  function resolveBoolean(from, paramValues) {
42
57
  if (typeof from === "boolean") {
43
58
  return from;
@@ -57,6 +72,10 @@ function isResourceInput(input) {
57
72
  return {}.hasOwnProperty.call(input, "resource");
58
73
  }
59
74
  exports.isResourceInput = isResourceInput;
75
+ function isMultiSelectInput(input) {
76
+ return {}.hasOwnProperty.call(input, "multiSelect");
77
+ }
78
+ exports.isMultiSelectInput = isMultiSelectInput;
60
79
  class ParamValue {
61
80
  constructor(rawValue, internal, types) {
62
81
  this.rawValue = rawValue;
@@ -64,16 +83,32 @@ class ParamValue {
64
83
  this.legalString = types.string || false;
65
84
  this.legalBoolean = types.boolean || false;
66
85
  this.legalNumber = types.number || false;
86
+ this.legalList = types.list || false;
87
+ this.delimiter = ",";
88
+ }
89
+ static fromList(ls, delimiter = ",") {
90
+ const pv = new ParamValue(ls.join(delimiter), false, { list: true });
91
+ pv.setDelimiter(delimiter);
92
+ return pv;
93
+ }
94
+ setDelimiter(delimiter) {
95
+ this.delimiter = delimiter;
67
96
  }
68
97
  toString() {
69
98
  return this.rawValue;
70
99
  }
100
+ toSDK() {
101
+ return this.legalList ? JSON.stringify(this.asList()) : this.toString();
102
+ }
71
103
  asString() {
72
104
  return this.rawValue;
73
105
  }
74
106
  asBoolean() {
75
107
  return ["true", "y", "yes", "1"].includes(this.rawValue);
76
108
  }
109
+ asList() {
110
+ return this.rawValue.split(this.delimiter);
111
+ }
77
112
  asNumber() {
78
113
  return +this.rawValue;
79
114
  }
@@ -94,6 +129,8 @@ function resolveDefaultCEL(type, expr, currentEnv) {
94
129
  return resolveString(expr, currentEnv);
95
130
  case "int":
96
131
  return resolveInt(expr, currentEnv);
132
+ case "list":
133
+ return resolveList(expr, currentEnv);
97
134
  default:
98
135
  throw new error_1.FirebaseError("Build specified parameter with default " + expr + " of unsupported type");
99
136
  }
@@ -108,6 +145,9 @@ function canSatisfyParam(param, value) {
108
145
  else if (param.type === "boolean") {
109
146
  return typeof value === "boolean";
110
147
  }
148
+ else if (param.type === "list") {
149
+ return Array.isArray(value);
150
+ }
111
151
  else if (param.type === "secret") {
112
152
  return false;
113
153
  }
@@ -147,7 +187,7 @@ async function resolveParams(params, firebaseConfig, userEnvs, nonInteractive) {
147
187
  exports.resolveParams = resolveParams;
148
188
  function populateDefaultParams(config) {
149
189
  const defaultParams = {};
150
- if (config.databaseURL !== "") {
190
+ if (config.databaseURL && config.databaseURL !== "") {
151
191
  defaultParams["DATABASE_URL"] = new ParamValue(config.databaseURL, true, {
152
192
  string: true,
153
193
  boolean: false,
@@ -164,7 +204,7 @@ function populateDefaultParams(config) {
164
204
  boolean: false,
165
205
  number: false,
166
206
  });
167
- if (config.storageBucket !== "") {
207
+ if (config.storageBucket && config.storageBucket !== "") {
168
208
  defaultParams["STORAGE_BUCKET"] = new ParamValue(config.storageBucket, true, {
169
209
  string: true,
170
210
  boolean: false,
@@ -207,11 +247,52 @@ async function promptParam(param, projectId, resolvedDefault) {
207
247
  const provided = await promptBooleanParam(param, resolvedDefault);
208
248
  return new ParamValue(provided.toString(), false, { boolean: true });
209
249
  }
250
+ else if (param.type === "list") {
251
+ const provided = await promptList(param, projectId, resolvedDefault);
252
+ return ParamValue.fromList(provided, param.delimiter);
253
+ }
210
254
  else if (param.type === "secret") {
211
255
  throw new error_1.FirebaseError(`Somehow ended up trying to interactively prompt for secret parameter ${param.name}, which should never happen.`);
212
256
  }
213
257
  (0, functional_1.assertExhaustive)(param);
214
258
  }
259
+ async function promptList(param, projectId, resolvedDefault) {
260
+ if (!param.input) {
261
+ const defaultToText = { text: {} };
262
+ param.input = defaultToText;
263
+ }
264
+ let prompt;
265
+ if (isSelectInput(param.input)) {
266
+ throw new error_1.FirebaseError("List params cannot have non-list selector inputs");
267
+ }
268
+ else if (isMultiSelectInput(param.input)) {
269
+ prompt = `Select a value for ${param.label || param.name}:`;
270
+ if (param.description) {
271
+ prompt += ` \n(${param.description})`;
272
+ }
273
+ prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
274
+ return promptSelectMultiple(prompt, param.input, resolvedDefault, (res) => res);
275
+ }
276
+ else if (isTextInput(param.input)) {
277
+ prompt = `Enter a list of strings (delimiter: ${param.delimiter ? param.delimiter : ","}) for ${param.label || param.name}:`;
278
+ if (param.description) {
279
+ prompt += ` \n(${param.description})`;
280
+ }
281
+ return promptText(prompt, param.input, resolvedDefault, (res) => {
282
+ return res.split(param.delimiter || ",");
283
+ });
284
+ }
285
+ else if (isResourceInput(param.input)) {
286
+ prompt = `Select values for ${param.label || param.name}:`;
287
+ if (param.description) {
288
+ prompt += ` \n(${param.description})`;
289
+ }
290
+ return promptResourceStrings(prompt, param.input, projectId);
291
+ }
292
+ else {
293
+ (0, functional_1.assertExhaustive)(param.input);
294
+ }
295
+ }
215
296
  async function promptBooleanParam(param, resolvedDefault) {
216
297
  if (!param.input) {
217
298
  const defaultToText = { text: {} };
@@ -227,6 +308,9 @@ async function promptBooleanParam(param, resolvedDefault) {
227
308
  prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
228
309
  return promptSelect(prompt, param.input, resolvedDefault, isTruthyInput);
229
310
  }
311
+ else if (isMultiSelectInput(param.input)) {
312
+ throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
313
+ }
230
314
  else if (isTextInput(param.input)) {
231
315
  prompt = `Enter a boolean value for ${param.label || param.name}:`;
232
316
  if (param.description) {
@@ -254,6 +338,9 @@ async function promptStringParam(param, projectId, resolvedDefault) {
254
338
  }
255
339
  return promptResourceString(prompt, param.input, projectId, resolvedDefault);
256
340
  }
341
+ else if (isMultiSelectInput(param.input)) {
342
+ throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
343
+ }
257
344
  else if (isSelectInput(param.input)) {
258
345
  prompt = `Select a value for ${param.label || param.name}:`;
259
346
  if (param.description) {
@@ -295,7 +382,10 @@ async function promptIntParam(param, resolvedDefault) {
295
382
  return +res;
296
383
  });
297
384
  }
298
- if (isTextInput(param.input)) {
385
+ else if (isMultiSelectInput(param.input)) {
386
+ throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
387
+ }
388
+ else if (isTextInput(param.input)) {
299
389
  prompt = `Enter an integer value for ${param.label || param.name}:`;
300
390
  if (param.description) {
301
391
  prompt += ` \n(${param.description})`;
@@ -338,6 +428,30 @@ async function promptResourceString(prompt, input, projectId, resolvedDefault) {
338
428
  return promptText(prompt, { text: {} }, resolvedDefault, (res) => res);
339
429
  }
340
430
  }
431
+ async function promptResourceStrings(prompt, input, projectId) {
432
+ const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
433
+ switch (input.resource.type) {
434
+ case "storage.googleapis.com/Bucket":
435
+ const buckets = await (0, storage_1.listBuckets)(projectId);
436
+ if (buckets.length === 0) {
437
+ throw notFound;
438
+ }
439
+ const forgedInput = {
440
+ multiSelect: {
441
+ options: buckets.map((bucketName) => {
442
+ return { label: bucketName, value: bucketName };
443
+ }),
444
+ },
445
+ };
446
+ return promptSelectMultiple(prompt, forgedInput, undefined, (res) => res);
447
+ default:
448
+ logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
449
+ return promptText(prompt, { text: {} }, undefined, (res) => res.split(","));
450
+ }
451
+ }
452
+ function shouldRetry(obj) {
453
+ return typeof obj === "object" && obj.message !== undefined;
454
+ }
341
455
  async function promptText(prompt, input, resolvedDefault, converter) {
342
456
  const res = await (0, prompt_1.promptOnce)({
343
457
  type: "input",
@@ -353,7 +467,7 @@ async function promptText(prompt, input, resolvedDefault, converter) {
353
467
  }
354
468
  }
355
469
  const converted = converter(res.toString());
356
- if (typeof converted === "object") {
470
+ if (shouldRetry(converted)) {
357
471
  logger_1.logger.error(converted.message);
358
472
  return promptText(prompt, input, resolvedDefault, converter);
359
473
  }
@@ -374,9 +488,30 @@ async function promptSelect(prompt, input, resolvedDefault, converter) {
374
488
  }),
375
489
  });
376
490
  const converted = converter(response);
377
- if (typeof converted === "object") {
491
+ if (shouldRetry(converted)) {
378
492
  logger_1.logger.error(converted.message);
379
493
  return promptSelect(prompt, input, resolvedDefault, converter);
380
494
  }
381
495
  return converted;
382
496
  }
497
+ async function promptSelectMultiple(prompt, input, resolvedDefault, converter) {
498
+ const response = await (0, prompt_1.promptOnce)({
499
+ name: "input",
500
+ type: "checkbox",
501
+ default: resolvedDefault,
502
+ message: prompt,
503
+ choices: input.multiSelect.options.map((option) => {
504
+ return {
505
+ checked: false,
506
+ name: option.label,
507
+ value: option.value.toString(),
508
+ };
509
+ }),
510
+ });
511
+ const converted = converter(response);
512
+ if (shouldRetry(converted)) {
513
+ logger_1.logger.error(converted.message);
514
+ return promptSelectMultiple(prompt, input, resolvedDefault, converter);
515
+ }
516
+ return converted;
517
+ }