firebase-tools 13.7.4 → 13.8.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 (88) hide show
  1. package/lib/api.js +9 -1
  2. package/lib/apiv2.js +19 -13
  3. package/lib/apphosting/app.js +4 -3
  4. package/lib/apphosting/githubConnections.js +1 -1
  5. package/lib/apphosting/index.js +70 -31
  6. package/lib/checkValidTargetFilters.js +8 -1
  7. package/lib/commands/apphosting-backends-create.js +3 -3
  8. package/lib/commands/apphosting-backends-delete.js +24 -17
  9. package/lib/commands/apphosting-backends-list.js +3 -3
  10. package/lib/commands/apphosting-secrets-grantaccess.js +9 -5
  11. package/lib/commands/dataconnect-list.js +64 -0
  12. package/lib/commands/dataconnect-sdk-generate.js +36 -0
  13. package/lib/commands/dataconnect-sql-diff.js +25 -0
  14. package/lib/commands/dataconnect-sql-migrate.js +41 -0
  15. package/lib/commands/deploy.js +27 -1
  16. package/lib/commands/index.js +10 -0
  17. package/lib/commands/init.js +7 -0
  18. package/lib/commands/setup-emulators-dataconnect.js +12 -0
  19. package/lib/config.js +1 -0
  20. package/lib/dataconnect/build.js +23 -0
  21. package/lib/dataconnect/checkIam.js +30 -0
  22. package/lib/dataconnect/client.js +115 -0
  23. package/lib/dataconnect/dataplaneClient.js +16 -0
  24. package/lib/dataconnect/ensureApis.js +12 -0
  25. package/lib/dataconnect/fileUtils.js +89 -0
  26. package/lib/dataconnect/filters.js +45 -0
  27. package/lib/dataconnect/freeTrial.js +23 -0
  28. package/lib/dataconnect/graphqlError.js +13 -0
  29. package/lib/dataconnect/load.js +40 -0
  30. package/lib/dataconnect/names.js +48 -0
  31. package/lib/dataconnect/prompts.js +20 -0
  32. package/lib/dataconnect/provisionCloudSql.js +91 -0
  33. package/lib/dataconnect/schemaMigration.js +137 -0
  34. package/lib/dataconnect/types.js +23 -0
  35. package/lib/deploy/dataconnect/deploy.js +84 -0
  36. package/lib/deploy/dataconnect/index.js +9 -0
  37. package/lib/deploy/dataconnect/prepare.js +30 -0
  38. package/lib/deploy/dataconnect/release.js +67 -0
  39. package/lib/deploy/functions/checkIam.js +4 -34
  40. package/lib/deploy/index.js +2 -0
  41. package/lib/downloadUtils.js +2 -2
  42. package/lib/emulator/constants.js +3 -0
  43. package/lib/emulator/controller.js +38 -12
  44. package/lib/emulator/dataconnectEmulator.js +86 -0
  45. package/lib/emulator/download.js +1 -1
  46. package/lib/emulator/downloadableEmulators.js +42 -3
  47. package/lib/emulator/portUtils.js +3 -2
  48. package/lib/emulator/registry.js +5 -0
  49. package/lib/emulator/types.js +3 -0
  50. package/lib/experiments.js +5 -0
  51. package/lib/extensions/emulator/specHelper.js +5 -39
  52. package/lib/gcp/apphosting.js +6 -1
  53. package/lib/gcp/cloudsql/cloudsqladmin.js +155 -0
  54. package/lib/gcp/cloudsql/connect.js +127 -0
  55. package/lib/gcp/cloudsql/fbToolsAuthClient.js +42 -0
  56. package/lib/gcp/cloudsql/types.js +2 -0
  57. package/lib/gcp/iam.js +33 -1
  58. package/lib/gcp/secretManager.js +1 -1
  59. package/lib/init/features/dataconnect/index.js +124 -0
  60. package/lib/init/features/emulators.js +13 -0
  61. package/lib/init/features/functions/index.js +15 -3
  62. package/lib/init/features/index.js +3 -1
  63. package/lib/init/index.js +1 -0
  64. package/lib/logger.js +22 -2
  65. package/lib/operation-poller.js +7 -1
  66. package/lib/rc.js +10 -1
  67. package/lib/requireAuth.js +1 -0
  68. package/lib/utils.js +51 -4
  69. package/package.json +6 -2
  70. package/schema/connector-yaml.json +54 -0
  71. package/schema/dataconnect-yaml.json +72 -0
  72. package/schema/firebase-config.json +103 -0
  73. package/templates/extensions/javascript/package.lint.json +2 -2
  74. package/templates/extensions/javascript/package.nolint.json +2 -2
  75. package/templates/extensions/typescript/package.lint.json +2 -2
  76. package/templates/extensions/typescript/package.nolint.json +2 -2
  77. package/templates/init/dataconnect/connector.yaml +2 -0
  78. package/templates/init/dataconnect/dataconnect.yaml +10 -0
  79. package/templates/init/dataconnect/mutations.gql +5 -0
  80. package/templates/init/dataconnect/queries.gql +7 -0
  81. package/templates/init/dataconnect/schema.gql +16 -0
  82. package/templates/init/functions/javascript/_gitignore +2 -1
  83. package/templates/init/functions/javascript/package.lint.json +2 -2
  84. package/templates/init/functions/javascript/package.nolint.json +2 -2
  85. package/templates/init/functions/python/_gitignore +1 -0
  86. package/templates/init/functions/typescript/_gitignore +1 -0
  87. package/templates/init/functions/typescript/package.lint.json +2 -2
  88. package/templates/init/functions/typescript/package.nolint.json +2 -2
package/lib/api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  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.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = 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.consoleOrigin = exports.authOrigin = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
4
- exports.setScopes = exports.getScopes = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = void 0;
4
+ exports.setScopes = exports.getScopes = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = void 0;
5
5
  const constants_1 = require("./emulator/constants");
6
6
  const logger_1 = require("./logger");
7
7
  const scopes = require("./scopes");
@@ -124,6 +124,14 @@ const githubClientId = () => utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6
124
124
  exports.githubClientId = githubClientId;
125
125
  const githubClientSecret = () => utils.envOverride("GITHUB_CLIENT_SECRET", "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0");
126
126
  exports.githubClientSecret = githubClientSecret;
127
+ const dataconnectOrigin = () => utils.envOverride("FIREBASE_DATACONNECT_URL", "https://firebasedataconnect.googleapis.com");
128
+ exports.dataconnectOrigin = dataconnectOrigin;
129
+ const dataConnectLocalConnString = () => utils.envOverride("FIREBASE_DATACONNECT_POSTGRESQL_STRING", "");
130
+ exports.dataConnectLocalConnString = dataConnectLocalConnString;
131
+ const cloudSQLAdminOrigin = () => utils.envOverride("CLOUD_SQL_URL", "https://sqladmin.googleapis.com");
132
+ exports.cloudSQLAdminOrigin = cloudSQLAdminOrigin;
133
+ const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatform.googleapis.com");
134
+ exports.vertexAIOrigin = vertexAIOrigin;
127
135
  function getScopes() {
128
136
  return Array.from(commandScopes);
129
137
  }
package/lib/apiv2.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Client = exports.setAccessToken = exports.setRefreshToken = void 0;
3
+ exports.Client = exports.getAccessToken = exports.setAccessToken = exports.setRefreshToken = exports.STANDARD_HEADERS = void 0;
4
4
  const url_1 = require("url");
5
5
  const stream_1 = require("stream");
6
6
  const proxy_agent_1 = require("proxy-agent");
@@ -15,6 +15,11 @@ 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
+ exports.STANDARD_HEADERS = {
19
+ Connection: "keep-alive",
20
+ "User-Agent": `FirebaseCLI/${CLI_VERSION}`,
21
+ "X-Client-Version": `FirebaseCLI/${CLI_VERSION}`,
22
+ };
18
23
  const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
19
24
  const GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
20
25
  const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
@@ -28,6 +33,14 @@ function setAccessToken(token = "") {
28
33
  accessToken = token;
29
34
  }
30
35
  exports.setAccessToken = setAccessToken;
36
+ async function getAccessToken() {
37
+ if (accessToken) {
38
+ return accessToken;
39
+ }
40
+ const data = await auth.getAccessToken(refreshToken, []);
41
+ return data.access_token;
42
+ }
43
+ exports.getAccessToken = getAccessToken;
31
44
  function proxyURIFromEnv() {
32
45
  return (process.env.HTTPS_PROXY ||
33
46
  process.env.https_proxy ||
@@ -125,11 +138,11 @@ class Client {
125
138
  if (!reqOptions.headers) {
126
139
  reqOptions.headers = new node_fetch_1.Headers();
127
140
  }
128
- reqOptions.headers.set("Connection", "keep-alive");
129
- if (!reqOptions.headers.has("User-Agent")) {
130
- reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`);
141
+ for (const [h, v] of Object.entries(exports.STANDARD_HEADERS)) {
142
+ if (!reqOptions.headers.has(h)) {
143
+ reqOptions.headers.set(h, v);
144
+ }
131
145
  }
132
- reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`);
133
146
  if (!reqOptions.headers.has("Content-Type")) {
134
147
  if (reqOptions.responseType === "json") {
135
148
  reqOptions.headers.set("Content-Type", "application/json");
@@ -151,18 +164,11 @@ class Client {
151
164
  token = "owner";
152
165
  }
153
166
  else {
154
- token = await this.getAccessToken();
167
+ token = await getAccessToken();
155
168
  }
156
169
  reqOptions.headers.set("Authorization", `Bearer ${token}`);
157
170
  return reqOptions;
158
171
  }
159
- async getAccessToken() {
160
- if (accessToken) {
161
- return accessToken;
162
- }
163
- const data = await auth.getAccessToken(refreshToken, []);
164
- return data.access_token;
165
- }
166
172
  requestURL(options) {
167
173
  const versionPath = this.opts.apiVersion ? `/${this.opts.apiVersion}` : "";
168
174
  return `${this.opts.urlPrefix}${versionPath}${options.path}`;
@@ -1,4 +1,6 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webApps = void 0;
2
4
  const fuzzy = require("fuzzy");
3
5
  const inquirer = require("inquirer");
4
6
  const apps_1 = require("../management/apps");
@@ -6,7 +8,7 @@ const prompt_1 = require("../prompt");
6
8
  const error_1 = require("../error");
7
9
  const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP";
8
10
  const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP";
9
- const webApps = {
11
+ exports.webApps = {
10
12
  CREATE_NEW_FIREBASE_WEB_APP,
11
13
  CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
12
14
  getOrCreateWebApp,
@@ -33,7 +35,7 @@ async function getOrCreateWebApp(projectId, firebaseWebAppName, backendId) {
33
35
  }
34
36
  return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName) };
35
37
  }
36
- return await webApps.promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps);
38
+ return await exports.webApps.promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps);
37
39
  }
38
40
  async function promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps) {
39
41
  const existingWebAppKeys = Array.from(existingUserProjectWebApps.keys());
@@ -89,4 +91,3 @@ function isQuotaError(error) {
89
91
  ((_e = (_d = (_c = original === null || original === void 0 ? void 0 : original.context) === null || _c === void 0 ? void 0 : _c.body) === null || _d === void 0 ? void 0 : _d.error) === null || _e === void 0 ? void 0 : _e.code);
90
92
  return code === 429;
91
93
  }
92
- module.exports = webApps;
@@ -77,7 +77,7 @@ async function linkGitHubRepository(projectId, location) {
77
77
  });
78
78
  const repo = await getOrCreateRepository(projectId, location, connectionId, repoCloneUri);
79
79
  utils.logSuccess(`Successfully linked GitHub repository at remote URI`);
80
- utils.logSuccess(`\t${repo.cloneUri}`);
80
+ utils.logSuccess(`\t${repo.cloneUri}\n`);
81
81
  return repo;
82
82
  }
83
83
  exports.linkGitHubRepository = linkGitHubRepository;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promptLocation = exports.deleteBackendAndPoll = exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
3
+ exports.getBackendForAmbiguousLocation = exports.promptLocation = exports.deleteBackendAndPoll = exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.ensureAppHostingComputeServiceAccount = exports.doSetup = void 0;
4
4
  const repo = require("./repo");
5
5
  const poller = require("../operation-poller");
6
6
  const apphosting = require("../gcp/apphosting");
@@ -15,7 +15,8 @@ const prompt_1 = require("../prompt");
15
15
  const constants_1 = require("./constants");
16
16
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
17
17
  const deploymentTool = require("../deploymentTool");
18
- const apps = require("./app");
18
+ const app_1 = require("./app");
19
+ const ora = require("ora");
19
20
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
20
21
  const apphostingPollerOptions = {
21
22
  apiOrigin: (0, api_1.apphostingOrigin)(),
@@ -23,21 +24,23 @@ const apphostingPollerOptions = {
23
24
  masterTimeout: 25 * 60 * 1000,
24
25
  maxBackoff: 10000,
25
26
  };
26
- async function doSetup(projectId, webAppName, location, serviceAccount, withDevConnect) {
27
+ async function doSetup(projectId, webAppName, location, serviceAccount, withCloudBuildRepos) {
27
28
  await Promise.all([
28
- ...(withDevConnect ? [(0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true)] : []),
29
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true),
29
30
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
30
31
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.secretManagerOrigin)(), "apphosting", true),
31
32
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudRunApiOrigin)(), "apphosting", true),
32
33
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.artifactRegistryDomain)(), "apphosting", true),
34
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.iamOrigin)(), "apphosting", true),
33
35
  ]);
36
+ (0, utils_1.logBullet)("First we need a few details to create your backend.\n");
37
+ await ensureAppHostingComputeServiceAccount(projectId, serviceAccount);
34
38
  const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
35
39
  if (location) {
36
40
  if (!allowedLocations.includes(location)) {
37
41
  throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
38
42
  }
39
43
  }
40
- (0, utils_1.logBullet)("First we need a few details to create your backend.\n");
41
44
  location =
42
45
  location || (await promptLocation(projectId, "Select a location to host your backend:\n"));
43
46
  (0, utils_1.logSuccess)(`Location set to ${location}.\n`);
@@ -47,23 +50,25 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withDevC
47
50
  default: "my-web-app",
48
51
  message: "Create a name for your backend [1-30 characters]",
49
52
  });
50
- const webApp = await apps.getOrCreateWebApp(projectId, webAppName, backendId);
53
+ const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName, backendId);
51
54
  if (webApp) {
52
55
  (0, utils_1.logSuccess)(`Firebase web app set to ${webApp.name}.\n`);
53
56
  }
54
57
  else {
55
58
  (0, utils_1.logWarning)(`Firebase web app not set`);
56
59
  }
57
- const gitRepositoryConnection = withDevConnect
58
- ? await githubConnections.linkGitHubRepository(projectId, location)
59
- : await repo.linkGitHubRepository(projectId, location);
60
+ const gitRepositoryConnection = withCloudBuildRepos
61
+ ? await repo.linkGitHubRepository(projectId, location)
62
+ : await githubConnections.linkGitHubRepository(projectId, location);
60
63
  const rootDir = await (0, prompt_1.promptOnce)({
61
64
  name: "rootDir",
62
65
  type: "input",
63
66
  default: "/",
64
67
  message: "Specify your app's root directory relative to your repository",
65
68
  });
69
+ const createBackendSpinner = ora("Creating your new backend...").start();
66
70
  const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount, webApp === null || webApp === void 0 ? void 0 : webApp.id, rootDir);
71
+ createBackendSpinner.succeed(`Successfully created backend:\n\t${backend.name}\n`);
67
72
  const branch = await (0, prompt_1.promptOnce)({
68
73
  name: "branch",
69
74
  type: "input",
@@ -78,10 +83,11 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withDevC
78
83
  message: "Do you want to deploy now?",
79
84
  });
80
85
  if (!confirmRollout) {
81
- (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
82
86
  (0, utils_1.logSuccess)(`Your backend will be deployed at:\n\thttps://${backend.uri}`);
83
87
  return;
84
88
  }
89
+ (0, utils_1.logBullet)(`You may also track this rollout at:\n\t${(0, api_1.consoleOrigin)()}/project/${projectId}/apphosting`);
90
+ const createRolloutSpinner = ora("Starting a new rollout... This make take a few minutes. It's safe to exit now.").start();
85
91
  await orchestrateRollout(projectId, location, backendId, {
86
92
  source: {
87
93
  codebase: {
@@ -89,10 +95,28 @@ async function doSetup(projectId, webAppName, location, serviceAccount, withDevC
89
95
  },
90
96
  },
91
97
  });
92
- (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
93
- (0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
98
+ createRolloutSpinner.succeed(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
94
99
  }
95
100
  exports.doSetup = doSetup;
101
+ async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount) {
102
+ const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId);
103
+ const name = `projects/${projectId}/serviceAccounts/${sa}`;
104
+ try {
105
+ await iam.testResourceIamPermissions((0, api_1.iamOrigin)(), "v1", name, ["iam.serviceAccounts.actAs"], `projects/${projectId}`);
106
+ }
107
+ catch (err) {
108
+ if (!(err instanceof error_1.FirebaseError)) {
109
+ throw err;
110
+ }
111
+ if (err.status === 404) {
112
+ await provisionDefaultComputeServiceAccount(projectId);
113
+ }
114
+ else if (err.status === 403) {
115
+ throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`, { original: err });
116
+ }
117
+ }
118
+ }
119
+ exports.ensureAppHostingComputeServiceAccount = ensureAppHostingComputeServiceAccount;
96
120
  async function promptNewBackendId(projectId, location, prompt) {
97
121
  while (true) {
98
122
  const backendId = await (0, prompt_1.promptOnce)(prompt);
@@ -127,33 +151,23 @@ async function createBackend(projectId, location, backendId, repository, service
127
151
  const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
128
152
  return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
129
153
  }
130
- try {
131
- return await createBackendAndPoll();
132
- }
133
- catch (err) {
134
- if (err.status === 403) {
135
- if (err.message.includes(defaultServiceAccount)) {
136
- await provisionDefaultComputeServiceAccount(projectId);
137
- return await createBackendAndPoll();
138
- }
139
- else if (serviceAccount && err.message.includes(serviceAccount)) {
140
- throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${serviceAccount}. Make sure you have the iam.serviceAccounts.actAs permission.`, { children: [err] });
141
- }
142
- }
143
- throw err;
144
- }
154
+ return await createBackendAndPoll();
145
155
  }
146
156
  exports.createBackend = createBackend;
147
157
  async function provisionDefaultComputeServiceAccount(projectId) {
148
158
  try {
149
- await iam.createServiceAccount(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
159
+ await iam.createServiceAccount(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Default service account used to run builds and deploys for Firebase App Hosting", "Firebase App Hosting compute service account");
150
160
  }
151
161
  catch (err) {
152
162
  if (err.status !== 409) {
153
163
  throw err;
154
164
  }
155
165
  }
156
- await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), ["roles/firebaseapphosting.computeRunner", "roles/firebase.sdkAdminServiceAgent"], true);
166
+ await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
167
+ "roles/firebaseapphosting.computeRunner",
168
+ "roles/firebase.sdkAdminServiceAgent",
169
+ "roles/developerconnect.readTokenAccessor",
170
+ ], true);
157
171
  }
158
172
  async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
159
173
  const traffic = {
@@ -172,7 +186,6 @@ async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseB
172
186
  }
173
187
  exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
174
188
  async function orchestrateRollout(projectId, location, backendId, buildInput) {
175
- (0, utils_1.logBullet)("Starting a new rollout... this may take a few minutes.");
176
189
  const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
177
190
  const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
178
191
  const rolloutBody = {
@@ -203,7 +216,6 @@ async function orchestrateRollout(projectId, location, backendId, buildInput) {
203
216
  const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, operationResourceName: rolloutOp.name }));
204
217
  const buildPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, operationResourceName: buildOp.name }));
205
218
  const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
206
- (0, utils_1.logSuccess)("Rollout completed.");
207
219
  if (build.state !== "READY") {
208
220
  if (!build.buildLogsUri) {
209
221
  throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
@@ -221,6 +233,9 @@ async function deleteBackendAndPoll(projectId, location, backendId) {
221
233
  exports.deleteBackendAndPoll = deleteBackendAndPoll;
222
234
  async function promptLocation(projectId, prompt = "Please select a location:") {
223
235
  const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
236
+ if (allowedLocations.length === 1) {
237
+ return allowedLocations[0];
238
+ }
224
239
  return (await (0, prompt_1.promptOnce)({
225
240
  name: "location",
226
241
  type: "list",
@@ -230,3 +245,27 @@ async function promptLocation(projectId, prompt = "Please select a location:") {
230
245
  }));
231
246
  }
232
247
  exports.promptLocation = promptLocation;
248
+ async function getBackendForAmbiguousLocation(projectId, backendId, locationDisambugationPrompt) {
249
+ let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
250
+ if (unreachable && unreachable.length !== 0) {
251
+ (0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable}.\n` +
252
+ "If your backend is in one of these regions, please try again later.");
253
+ }
254
+ backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
255
+ if (backends.length === 0) {
256
+ throw new error_1.FirebaseError(`No backend named "${backendId}" found.`);
257
+ }
258
+ if (backends.length === 1) {
259
+ return backends[0];
260
+ }
261
+ const backendsByLocation = new Map();
262
+ backends.forEach((backend) => backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend));
263
+ const location = await (0, prompt_1.promptOnce)({
264
+ name: "location",
265
+ type: "list",
266
+ message: locationDisambugationPrompt,
267
+ choices: [...backendsByLocation.keys()],
268
+ });
269
+ return backendsByLocation.get(location);
270
+ }
271
+ exports.getBackendForAmbiguousLocation = getBackendForAmbiguousLocation;
@@ -19,7 +19,14 @@ 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
+ const FILTERABLE_TARGETS = new Set([
23
+ "hosting",
24
+ "functions",
25
+ "firestore",
26
+ "storage",
27
+ "database",
28
+ "dataconnect",
29
+ ]);
23
30
  async function checkValidTargetFilters(options) {
24
31
  const only = !options.only ? [] : options.only.split(",");
25
32
  return new Promise((resolve, reject) => {
@@ -11,7 +11,7 @@ exports.command = new command_1.Command("apphosting:backends:create")
11
11
  .option("-a, --app <webApp>", "specify an existing Firebase web app to associate your App Hosting backend with")
12
12
  .option("-l, --location <location>", "specify the location of the backend", "")
13
13
  .option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
14
- .option("-w, --with-dev-connect", "use the Developer Connect flow insetad of Cloud Build Repositories (testing)", false)
14
+ .option("-w, --with-cloud-build-repos", "use Cloud Build Repositories flow instead of the Developer Connect flow")
15
15
  .before(apphosting_2.ensureApiEnabled)
16
16
  .before(requireInteractive_1.default)
17
17
  .action(async (options) => {
@@ -19,6 +19,6 @@ exports.command = new command_1.Command("apphosting:backends:create")
19
19
  const webApp = options.app;
20
20
  const location = options.location;
21
21
  const serviceAccount = options.serviceAccount;
22
- const withDevConnect = options.withDevConnect;
23
- await (0, apphosting_1.doSetup)(projectId, webApp, location, serviceAccount, withDevConnect);
22
+ const withCloudBuildRepos = options.withCloudBuildRepos;
23
+ await (0, apphosting_1.doSetup)(projectId, webApp, location, serviceAccount, withCloudBuildRepos);
24
24
  });
@@ -9,29 +9,25 @@ const utils = require("../utils");
9
9
  const apphosting = require("../gcp/apphosting");
10
10
  const apphosting_backends_list_1 = require("./apphosting-backends-list");
11
11
  const apphosting_1 = require("../apphosting");
12
+ const ora = require("ora");
12
13
  exports.command = new command_1.Command("apphosting:backends:delete <backend>")
13
14
  .description("delete a Firebase App Hosting backend")
14
- .option("-l, --location <location>", "specify the location of the backend", "")
15
+ .option("-l, --location <location>", "specify the location of the backend", "-")
15
16
  .withForce()
16
17
  .before(apphosting.ensureApiEnabled)
17
18
  .action(async (backendId, options) => {
18
19
  const projectId = (0, projectUtils_1.needProjectId)(options);
19
20
  let location = options.location;
20
- location =
21
- location ||
22
- (await (0, apphosting_1.promptLocation)(projectId, "Please select the location of the backend you'd like to delete:"));
23
21
  let backend;
24
- try {
25
- backend = await apphosting.getBackend(projectId, location, backendId);
22
+ if (location === "-" || location === "") {
23
+ backend = await (0, apphosting_1.getBackendForAmbiguousLocation)(projectId, backendId, "Please select the location of the backend you'd like to delete:");
24
+ location = apphosting.parseBackendName(backend.name).location;
26
25
  }
27
- catch (err) {
28
- throw new error_1.FirebaseError(`No backends found with given parameters. Command aborted.`, {
29
- original: err,
30
- });
26
+ else {
27
+ backend = await getBackendForLocation(projectId, location, backendId);
31
28
  }
32
- utils.logWarning("You are about to permanently delete the backend:");
33
- const backends = [backend];
34
- (0, apphosting_backends_list_1.printBackendsTable)(backends);
29
+ utils.logWarning("You are about to permanently delete this backend:");
30
+ (0, apphosting_backends_list_1.printBackendsTable)([backend]);
35
31
  const confirmDeletion = await (0, prompt_1.promptOnce)({
36
32
  type: "confirm",
37
33
  name: "force",
@@ -39,14 +35,25 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
39
35
  message: "Are you sure?",
40
36
  }, options);
41
37
  if (!confirmDeletion) {
42
- throw new error_1.FirebaseError("Deletion Aborted");
38
+ return;
43
39
  }
40
+ const spinner = ora("Deleting backend...").start();
44
41
  try {
45
42
  await (0, apphosting_1.deleteBackendAndPoll)(projectId, location, backendId);
46
- utils.logSuccess(`Successfully deleted the backend: ${backendId}`);
43
+ spinner.succeed(`Successfully deleted the backend: ${backendId}`);
47
44
  }
48
45
  catch (err) {
49
- throw new error_1.FirebaseError(`Failed to delete backend: ${backendId}. Please check the parameters you have provided.`, { original: err });
46
+ spinner.stop();
47
+ throw new error_1.FirebaseError(`Failed to delete backend: ${backendId}.`, { original: err });
50
48
  }
51
- return backend;
52
49
  });
50
+ async function getBackendForLocation(projectId, location, backendId) {
51
+ try {
52
+ return await apphosting.getBackend(projectId, location, backendId);
53
+ }
54
+ catch (err) {
55
+ throw new error_1.FirebaseError(`No backend named "${backendId}" found in ${location}.`, {
56
+ original: err,
57
+ });
58
+ }
59
+ }
@@ -35,12 +35,12 @@ function printBackendsTable(backends) {
35
35
  style: { head: ["green"] },
36
36
  });
37
37
  for (const backend of backends) {
38
- const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
38
+ const { location, id } = apphosting.parseBackendName(backend.name);
39
39
  table.push([
40
- backendId,
40
+ id,
41
41
  (_c = (_b = (_a = backend.codebase) === null || _a === void 0 ? void 0 : _a.repository) === null || _b === void 0 ? void 0 : _b.split("/").pop()) !== null && _c !== void 0 ? _c : "",
42
42
  backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
43
- backendLocation,
43
+ location,
44
44
  (0, utils_1.datetimeString)(new Date(backend.updateTime)),
45
45
  ]);
46
46
  }
@@ -12,7 +12,7 @@ const secrets = require("../apphosting/secrets");
12
12
  const apphosting_1 = require("../apphosting");
13
13
  exports.command = new command_1.Command("apphosting:secrets:grantaccess <secretName>")
14
14
  .description("grant service accounts permissions to the provided secret")
15
- .option("-l, --location <location>", "backend location")
15
+ .option("-l, --location <location>", "backend location", "-")
16
16
  .option("-b, --backend <backend>", "backend name")
17
17
  .before(requireAuth_1.requireAuth)
18
18
  .before(secretManager.ensureApi)
@@ -31,15 +31,19 @@ exports.command = new command_1.Command("apphosting:secrets:grantaccess <secretN
31
31
  if (!options.backend) {
32
32
  throw new error_1.FirebaseError("Missing required flag --backend. See firebase apphosting:secrets:grantaccess --help for more info");
33
33
  }
34
- let location = options.location;
35
- location =
36
- location || (await (0, apphosting_1.promptLocation)(projectId, "Please select the location of your backend"));
37
34
  const exists = await secretManager.secretExists(projectId, secretName);
38
35
  if (!exists) {
39
36
  throw new error_1.FirebaseError(`Cannot find secret ${secretName}`);
40
37
  }
41
38
  const backendId = options.backend;
42
- const backend = await apphosting.getBackend(projectId, location, backendId);
39
+ const location = options.location;
40
+ let backend;
41
+ if (location === "" || location === "-") {
42
+ backend = await (0, apphosting_1.getBackendForAmbiguousLocation)(projectId, backendId, "Please select the location of your backend:");
43
+ }
44
+ else {
45
+ backend = await apphosting.getBackend(projectId, location, backendId);
46
+ }
43
47
  const accounts = secrets.toMulti(secrets.serviceAccountsForBackend(projectNumber, backend));
44
48
  await secrets.grantSecretAccess(projectId, projectNumber, secretName, accounts);
45
49
  });
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const projectUtils_1 = require("../projectUtils");
6
+ const names = require("../dataconnect/names");
7
+ const client = require("../dataconnect/client");
8
+ const logger_1 = require("../logger");
9
+ const requirePermissions_1 = require("../requirePermissions");
10
+ const ensureApis_1 = require("../dataconnect/ensureApis");
11
+ const Table = require("cli-table");
12
+ exports.command = new command_1.Command("dataconnect:list")
13
+ .description("list all deployed services in your Firebase project")
14
+ .before(requirePermissions_1.requirePermissions, [
15
+ "dataconnect.services.list",
16
+ "dataconnect.schemas.list",
17
+ "dataconnect.connectors.list",
18
+ ])
19
+ .action(async (options) => {
20
+ var _a, _b, _c, _d, _e;
21
+ const projectId = (0, projectUtils_1.needProjectId)(options);
22
+ await (0, ensureApis_1.ensureApis)(projectId);
23
+ const services = await client.listAllServices(projectId);
24
+ const table = new Table({
25
+ head: [
26
+ "Service ID",
27
+ "Location",
28
+ "Data Source",
29
+ "Schema Last Updated",
30
+ "Connector ID",
31
+ "Connector Last Updated",
32
+ ],
33
+ style: { head: ["yellow"] },
34
+ });
35
+ const jsonOutput = { services: [] };
36
+ for (const service of services) {
37
+ const schema = await client.getSchema(service.name);
38
+ const connectors = await client.listConnectors(service.name);
39
+ const serviceName = names.parseServiceName(service.name);
40
+ const instanceName = (_b = (_a = schema === null || schema === void 0 ? void 0 : schema.primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instance) !== null && _b !== void 0 ? _b : "";
41
+ const instanceId = instanceName.split("/").pop();
42
+ const dbId = (_d = (_c = schema === null || schema === void 0 ? void 0 : schema.primaryDatasource.postgresql) === null || _c === void 0 ? void 0 : _c.database) !== null && _d !== void 0 ? _d : "";
43
+ const dbName = `CloudSQL Instance: ${instanceId} Database:${dbId}`;
44
+ table.push([serviceName.serviceId, serviceName.location, dbName, schema === null || schema === void 0 ? void 0 : schema.updateTime, "", ""]);
45
+ const serviceJson = {
46
+ serviceId: serviceName.serviceId,
47
+ location: serviceName.location,
48
+ datasource: dbName,
49
+ schemaUpdateTime: schema === null || schema === void 0 ? void 0 : schema.updateTime,
50
+ connectors: [],
51
+ };
52
+ for (const conn of connectors) {
53
+ const connectorName = names.parseConnectorName(conn.name);
54
+ table.push(["", "", "", "", connectorName.connectorId, conn.updateTime]);
55
+ serviceJson.connectors.push({
56
+ connectorId: connectorName.connectorId,
57
+ connectorLastUpdated: (_e = conn.updateTime) !== null && _e !== void 0 ? _e : "",
58
+ });
59
+ }
60
+ jsonOutput.services.push(serviceJson);
61
+ }
62
+ logger_1.logger.info(table.toString());
63
+ return jsonOutput;
64
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const path = require("path");
5
+ const command_1 = require("../command");
6
+ const dataconnectEmulator_1 = require("../emulator/dataconnectEmulator");
7
+ const projectUtils_1 = require("../projectUtils");
8
+ const load_1 = require("../dataconnect/load");
9
+ const fileUtils_1 = require("../dataconnect/fileUtils");
10
+ const logger_1 = require("../logger");
11
+ exports.command = new command_1.Command("dataconnect:sdk:generate")
12
+ .description("generates typed SDKs for your Data Connect connectors")
13
+ .action(async (options) => {
14
+ const projectId = (0, projectUtils_1.needProjectId)(options);
15
+ const services = (0, fileUtils_1.readFirebaseJson)(options.config);
16
+ for (const service of services) {
17
+ let configDir = service.source;
18
+ if (!path.isAbsolute(configDir)) {
19
+ const cwd = options.cwd || process.cwd();
20
+ configDir = path.resolve(path.join(cwd), configDir);
21
+ }
22
+ const serviceInfo = await (0, load_1.load)(projectId, service.location, configDir);
23
+ const args = {
24
+ projectId,
25
+ configDir,
26
+ auto_download: true,
27
+ rc: options.rc,
28
+ };
29
+ const dataconnectEmulator = new dataconnectEmulator_1.DataConnectEmulator(args);
30
+ for (const conn of serviceInfo.connectorInfo) {
31
+ const output = await dataconnectEmulator.generate(conn.connectorYaml.connectorId);
32
+ console.log(output);
33
+ logger_1.logger.info(`Generated SDKs for ${conn.connectorYaml.connectorId}`);
34
+ }
35
+ }
36
+ });
@@ -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 projectUtils_1 = require("../projectUtils");
6
+ const ensureApis_1 = require("../dataconnect/ensureApis");
7
+ const requirePermissions_1 = require("../requirePermissions");
8
+ const fileUtils_1 = require("../dataconnect/fileUtils");
9
+ const schemaMigration_1 = require("../dataconnect/schemaMigration");
10
+ const requireAuth_1 = require("../requireAuth");
11
+ exports.command = new command_1.Command("dataconnect:sql:diff [serviceId]")
12
+ .description("displays the differences between a local DataConnect schema and your CloudSQL database's current schema")
13
+ .before(requirePermissions_1.requirePermissions, [
14
+ "firebasedataconnect.services.list",
15
+ "firebasedataconnect.schemas.list",
16
+ "firebasedataconnect.schemas.update",
17
+ ])
18
+ .before(requireAuth_1.requireAuth)
19
+ .action(async (serviceId, options) => {
20
+ const projectId = (0, projectUtils_1.needProjectId)(options);
21
+ await (0, ensureApis_1.ensureApis)(projectId);
22
+ const serviceInfo = await (0, fileUtils_1.pickService)(projectId, options.config, serviceId);
23
+ const diffs = await (0, schemaMigration_1.diffSchema)(serviceInfo.schema);
24
+ return { projectId, serviceId, diffs };
25
+ });