firebase-tools 14.12.1 → 14.14.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 (56) hide show
  1. package/README.md +1 -1
  2. package/lib/commands/dataconnect-services-list.js +5 -5
  3. package/lib/commands/dataconnect-sql-grant.js +5 -0
  4. package/lib/commands/dataconnect-sql-setup.js +1 -3
  5. package/lib/crashlytics/getIssueDetails.js +41 -0
  6. package/lib/crashlytics/getSampleCrash.js +48 -0
  7. package/lib/dataconnect/client.js +23 -15
  8. package/lib/dataconnect/ensureApis.js +5 -9
  9. package/lib/dataconnect/errors.js +7 -1
  10. package/lib/dataconnect/fileUtils.js +5 -6
  11. package/lib/dataconnect/freeTrial.js +16 -39
  12. package/lib/dataconnect/provisionCloudSql.js +67 -70
  13. package/lib/dataconnect/schemaMigration.js +222 -170
  14. package/lib/deploy/dataconnect/deploy.js +9 -11
  15. package/lib/deploy/dataconnect/prepare.js +7 -10
  16. package/lib/deploy/dataconnect/release.js +42 -30
  17. package/lib/deploy/functions/backend.js +8 -2
  18. package/lib/deploy/functions/build.js +23 -1
  19. package/lib/deploy/functions/ensure.js +1 -1
  20. package/lib/deploy/functions/functionsDeployHelper.js +8 -1
  21. package/lib/deploy/functions/prepare.js +8 -4
  22. package/lib/deploy/functions/pricing.js +12 -5
  23. package/lib/deploy/functions/release/fabricator.js +25 -3
  24. package/lib/emulator/controller.js +7 -3
  25. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  26. package/lib/emulator/functionsEmulator.js +11 -1
  27. package/lib/experiments.js +4 -0
  28. package/lib/extensions/extensionsHelper.js +4 -15
  29. package/lib/extensions/utils.js +1 -12
  30. package/lib/firestore/api.js +25 -11
  31. package/lib/firestore/pretty-print.js +7 -0
  32. package/lib/functional.js +7 -1
  33. package/lib/functions/env.js +19 -15
  34. package/lib/functions/projectConfig.js +25 -2
  35. package/lib/functions/secrets.js +3 -0
  36. package/lib/gcp/cloudfunctionsv2.js +3 -31
  37. package/lib/gcp/cloudscheduler.js +1 -1
  38. package/lib/gcp/cloudsql/cloudsqladmin.js +2 -14
  39. package/lib/gcp/cloudsql/connect.js +3 -2
  40. package/lib/gcp/cloudsql/permissionsSetup.js +23 -16
  41. package/lib/gcp/k8s.js +32 -0
  42. package/lib/gcp/runv2.js +178 -0
  43. package/lib/gemini/fdcExperience.js +5 -3
  44. package/lib/init/features/dataconnect/index.js +266 -162
  45. package/lib/init/features/dataconnect/sdk.js +36 -20
  46. package/lib/init/features/project.js +4 -0
  47. package/lib/management/studio.js +1 -1
  48. package/lib/mcp/tools/core/init.js +7 -6
  49. package/lib/mcp/tools/crashlytics/get_issue_details.js +33 -0
  50. package/lib/mcp/tools/crashlytics/get_sample_crash.js +43 -0
  51. package/lib/mcp/tools/crashlytics/index.js +7 -1
  52. package/lib/mcp/tools/crashlytics/list_top_issues.js +2 -1
  53. package/lib/rtdb.js +1 -1
  54. package/package.json +1 -1
  55. package/schema/firebase-config.json +6 -0
  56. package/lib/extensions/resolveSource.js +0 -24
@@ -16,97 +16,121 @@ const utils_1 = require("../utils");
16
16
  const cloudsqladmin_1 = require("../gcp/cloudsql/cloudsqladmin");
17
17
  const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
18
18
  const errors = require("./errors");
19
+ const provisionCloudSql_1 = require("./provisionCloudSql");
20
+ const requireAuth_1 = require("../requireAuth");
19
21
  async function setupSchemaIfNecessary(instanceId, databaseId, options) {
20
- await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
21
- const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
22
- if (schemaInfo.setupStatus !== permissionsSetup_1.SchemaSetupStatus.BrownField &&
23
- schemaInfo.setupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField) {
24
- return await (0, permissionsSetup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options, true);
22
+ try {
23
+ await (0, connect_1.setupIAMUsers)(instanceId, options);
24
+ const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
25
+ switch (schemaInfo.setupStatus) {
26
+ case permissionsSetup_1.SchemaSetupStatus.BrownField:
27
+ case permissionsSetup_1.SchemaSetupStatus.GreenField:
28
+ logger_1.logger.debug(`Cloud SQL Database ${instanceId}:${databaseId} is already set up in ${schemaInfo.setupStatus}`);
29
+ return schemaInfo.setupStatus;
30
+ case permissionsSetup_1.SchemaSetupStatus.NotSetup:
31
+ case permissionsSetup_1.SchemaSetupStatus.NotFound:
32
+ (0, utils_1.logLabeledBullet)("dataconnect", "Setting up Cloud SQL Database SQL permissions...");
33
+ return await (0, permissionsSetup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options, true);
34
+ default:
35
+ throw new error_1.FirebaseError(`Unexpected schema setup status: ${schemaInfo.setupStatus}`);
36
+ }
25
37
  }
26
- else {
27
- logger_1.logger.debug(`Detected schema "${schemaInfo.name}" is setup in ${schemaInfo.setupStatus} mode. Skipping Setup.`);
38
+ catch (err) {
39
+ throw new error_1.FirebaseError(`Cannot setup Postgres SQL permissions of Cloud SQL database ${instanceId}:${databaseId}\n${err}`);
28
40
  }
29
- return schemaInfo.setupStatus;
30
41
  }
31
42
  async function diffSchema(options, schema, schemaValidation) {
32
- (0, utils_1.logLabeledBullet)("dataconnect", `generating required schema changes...`);
43
+ let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "STRICT";
44
+ setSchemaValidationMode(schema, validationMode);
45
+ displayStartSchemaDiff(validationMode);
33
46
  const { serviceName, instanceName, databaseId, instanceId } = getIdentifiers(schema);
34
47
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
35
- let diffs = [];
36
- await setupSchemaIfNecessary(instanceId, databaseId, options);
37
- let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
38
- setSchemaValidationMode(schema, validationMode);
48
+ let incompatible = undefined;
39
49
  try {
40
50
  await (0, client_1.upsertSchema)(schema, true);
41
- if (validationMode === "STRICT") {
42
- (0, utils_1.logLabeledSuccess)("dataconnect", `database schema of ${instanceId}:${databaseId} is up to date.`);
43
- }
44
- else {
45
- (0, utils_1.logLabeledSuccess)("dataconnect", `database schema of ${instanceId}:${databaseId} is compatible.`);
46
- }
51
+ displayNoSchemaDiff(instanceId, databaseId, validationMode);
47
52
  }
48
53
  catch (err) {
49
54
  if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
50
55
  throw err;
51
56
  }
57
+ incompatible = errors.getIncompatibleSchemaError(err);
52
58
  const invalidConnectors = errors.getInvalidConnectors(err);
53
- const incompatible = errors.getIncompatibleSchemaError(err);
54
59
  if (!incompatible && !invalidConnectors.length) {
60
+ const gqlErrs = errors.getGQLErrors(err);
61
+ if (gqlErrs) {
62
+ throw new error_1.FirebaseError(`There are errors in your schema files:\n${gqlErrs}`);
63
+ }
55
64
  throw err;
56
65
  }
57
66
  if (invalidConnectors.length) {
58
67
  displayInvalidConnectors(invalidConnectors);
59
68
  }
60
- if (incompatible) {
61
- displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
62
- diffs = incompatible.diffs;
63
- }
64
69
  }
65
- if (!schemaValidation) {
66
- validationMode = "STRICT";
67
- setSchemaValidationMode(schema, validationMode);
68
- try {
69
- (0, utils_1.logLabeledBullet)("dataconnect", `generating schema changes, including optional changes...`);
70
- await (0, client_1.upsertSchema)(schema, true);
71
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
72
- }
73
- catch (err) {
74
- if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
75
- throw err;
76
- }
77
- const incompatible = errors.getIncompatibleSchemaError(err);
78
- if (incompatible) {
79
- if (!diffsEqual(diffs, incompatible.diffs)) {
80
- if (diffs.length === 0) {
81
- displaySchemaChanges(incompatible, "STRICT_AFTER_COMPATIBLE", instanceName, databaseId);
82
- }
83
- else {
84
- displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
85
- }
86
- diffs = incompatible.diffs;
87
- }
88
- else {
89
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
90
- }
91
- }
70
+ if (!incompatible) {
71
+ return [];
72
+ }
73
+ if (schemaValidation) {
74
+ displaySchemaChanges(incompatible, validationMode);
75
+ return incompatible.diffs;
76
+ }
77
+ const strictIncompatible = incompatible;
78
+ let compatibleIncompatible = undefined;
79
+ validationMode = "COMPATIBLE";
80
+ setSchemaValidationMode(schema, validationMode);
81
+ try {
82
+ displayStartSchemaDiff(validationMode);
83
+ await (0, client_1.upsertSchema)(schema, true);
84
+ displayNoSchemaDiff(instanceId, databaseId, validationMode);
85
+ }
86
+ catch (err) {
87
+ if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
88
+ throw err;
92
89
  }
90
+ compatibleIncompatible = errors.getIncompatibleSchemaError(err);
93
91
  }
94
- return diffs;
92
+ if (!compatibleIncompatible) {
93
+ displaySchemaChanges(strictIncompatible, "STRICT");
94
+ }
95
+ else if (diffsEqual(strictIncompatible.diffs, compatibleIncompatible.diffs)) {
96
+ displaySchemaChanges(strictIncompatible, "STRICT");
97
+ }
98
+ else {
99
+ displaySchemaChanges(compatibleIncompatible, "COMPATIBLE");
100
+ displaySchemaChanges(strictIncompatible, "STRICT_AFTER_COMPATIBLE");
101
+ }
102
+ return incompatible.diffs;
95
103
  }
96
104
  exports.diffSchema = diffSchema;
97
105
  async function migrateSchema(args) {
98
- (0, utils_1.logLabeledBullet)("dataconnect", `generating required schema changes...`);
106
+ var _a;
99
107
  const { options, schema, validateOnly, schemaValidation } = args;
108
+ let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
109
+ setSchemaValidationMode(schema, validationMode);
110
+ displayStartSchemaDiff(validationMode);
111
+ const projectId = (0, projectUtils_1.needProjectId)(options);
100
112
  const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
101
113
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true);
102
- await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
103
- let diffs = [];
114
+ const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
115
+ if (existingInstance.state === "PENDING_CREATE") {
116
+ const postgresql = (_a = schema.datasources.find((d) => d.postgresql)) === null || _a === void 0 ? void 0 : _a.postgresql;
117
+ if (!postgresql) {
118
+ throw new error_1.FirebaseError(`Cannot find Postgres datasource in the schema to deploy: ${serviceName}/schemas/${types_1.SCHEMA_ID}.\nIts datasources: ${JSON.stringify(schema.datasources)}`);
119
+ }
120
+ postgresql.schemaValidation = "NONE";
121
+ postgresql.schemaMigration = undefined;
122
+ await (0, client_1.upsertSchema)(schema, validateOnly);
123
+ postgresql.schemaValidation = undefined;
124
+ postgresql.schemaMigration = "MIGRATE_COMPATIBLE";
125
+ await (0, client_1.upsertSchema)(schema, validateOnly, true);
126
+ (0, utils_1.logLabeledWarning)("dataconnect", `Skip SQL schema migration because Cloud SQL is still being created`);
127
+ return [];
128
+ }
104
129
  await setupSchemaIfNecessary(instanceId, databaseId, options);
105
- let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
106
- setSchemaValidationMode(schema, validationMode);
130
+ let diffs = [];
107
131
  try {
108
132
  await (0, client_1.upsertSchema)(schema, validateOnly);
109
- (0, utils_1.logLabeledBullet)("dataconnect", `database schema of ${instanceId}:${databaseId} is up to date.`);
133
+ displayNoSchemaDiff(instanceId, databaseId, validationMode);
110
134
  }
111
135
  catch (err) {
112
136
  if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
@@ -115,9 +139,13 @@ async function migrateSchema(args) {
115
139
  const incompatible = errors.getIncompatibleSchemaError(err);
116
140
  const invalidConnectors = errors.getInvalidConnectors(err);
117
141
  if (!incompatible && !invalidConnectors.length) {
142
+ const gqlErrs = errors.getGQLErrors(err);
143
+ if (gqlErrs) {
144
+ throw new error_1.FirebaseError(`There are errors in your schema files:\n${gqlErrs}`);
145
+ }
118
146
  throw err;
119
147
  }
120
- const migrationMode = await promptForSchemaMigration(options, instanceName, databaseId, incompatible, validateOnly, validationMode);
148
+ const migrationMode = await promptForSchemaMigration(options, instanceId, databaseId, incompatible, validateOnly, validationMode);
121
149
  const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(options, serviceName, invalidConnectors, validateOnly);
122
150
  if (incompatible) {
123
151
  diffs = await handleIncompatibleSchemaError({
@@ -150,7 +178,7 @@ async function migrateSchema(args) {
150
178
  if (!incompatible && !invalidConnectors.length) {
151
179
  throw err;
152
180
  }
153
- const migrationMode = await promptForSchemaMigration(options, instanceName, databaseId, incompatible, validateOnly, "STRICT_AFTER_COMPATIBLE");
181
+ const migrationMode = await promptForSchemaMigration(options, instanceId, databaseId, incompatible, validateOnly, "STRICT_AFTER_COMPATIBLE");
154
182
  if (incompatible) {
155
183
  const maybeDiffs = await handleIncompatibleSchemaError({
156
184
  options,
@@ -169,22 +197,13 @@ exports.migrateSchema = migrateSchema;
169
197
  async function grantRoleToUserInSchema(options, schema) {
170
198
  const role = options.role;
171
199
  const email = options.email;
172
- const { instanceId, databaseId } = getIdentifiers(schema);
173
- const projectId = (0, projectUtils_1.needProjectId)(options);
174
- const { user, mode } = (0, connect_1.toDatabaseUser)(email);
175
- const fdcSqlRole = permissionsSetup_1.fdcSqlRoleMap[role](databaseId);
176
- await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
177
- const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
178
- if (!userIsCSQLAdmin) {
179
- throw new error_1.FirebaseError(`Only users with 'roles/cloudsql.admin' can grant SQL roles. If you do not have this role, ask your database administrator to run this command or manually grant ${fdcSqlRole} to ${user}`);
180
- }
200
+ const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
201
+ await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
181
202
  const schemaSetupStatus = await setupSchemaIfNecessary(instanceId, databaseId, options);
182
- if (schemaSetupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField &&
183
- fdcSqlRole === (0, permissions_1.firebaseowner)(databaseId, permissions_1.DEFAULT_SCHEMA)) {
184
- throw new error_1.FirebaseError(`Owner rule isn't available in brownfield databases. If you would like Data Connect to manage and own your database schema, run 'firebase dataconnect:sql:setup'`);
203
+ if (schemaSetupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField && role === "owner") {
204
+ throw new error_1.FirebaseError(`Owner rule isn't available in ${schemaSetupStatus} databases. If you would like Data Connect to manage and own your database schema, run 'firebase dataconnect:sql:setup'`);
185
205
  }
186
- await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
187
- await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
206
+ await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, role, email);
188
207
  }
189
208
  exports.grantRoleToUserInSchema = grantRoleToUserInSchema;
190
209
  function diffsEqual(x, y) {
@@ -207,15 +226,15 @@ function setSchemaValidationMode(schema, schemaValidation) {
207
226
  }
208
227
  }
209
228
  function getIdentifiers(schema) {
210
- var _a, _b;
229
+ var _a, _b, _c;
211
230
  const postgresDatasource = schema.datasources.find((d) => d.postgresql);
212
231
  const databaseId = (_a = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.database;
213
232
  if (!databaseId) {
214
- throw new error_1.FirebaseError("Service does not have a postgres datasource, cannot migrate");
233
+ throw new error_1.FirebaseError("Data Connect schema must have a postgres datasource with a database name.");
215
234
  }
216
- const instanceName = (_b = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql.instance;
235
+ const instanceName = (_c = (_b = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql) === null || _c === void 0 ? void 0 : _c.instance;
217
236
  if (!instanceName) {
218
- throw new error_1.FirebaseError("tried to migrate schema but instance name was not provided in dataconnect.yaml");
237
+ throw new error_1.FirebaseError("Data Connect schema must have a postgres datasource with a CloudSQL instance.");
219
238
  }
220
239
  const instanceId = instanceName.split("/").pop();
221
240
  const serviceName = schema.name.replace(`/schemas/${types_1.SCHEMA_ID}`, "");
@@ -235,11 +254,7 @@ function suggestedCommand(serviceName, invalidConnectorNames) {
235
254
  }
236
255
  async function handleIncompatibleSchemaError(args) {
237
256
  const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args;
238
- if (incompatibleSchemaError.destructive && choice === "safe") {
239
- throw new error_1.FirebaseError("This schema migration includes potentially destructive changes. If you'd like to execute it anyway, rerun this command with --force");
240
- }
241
- const commandsToExecute = incompatibleSchemaError.diffs
242
- .filter((d) => {
257
+ const commandsToExecute = incompatibleSchemaError.diffs.filter((d) => {
243
258
  switch (choice) {
244
259
  case "all":
245
260
  return true;
@@ -248,16 +263,15 @@ async function handleIncompatibleSchemaError(args) {
248
263
  case "none":
249
264
  return false;
250
265
  }
251
- })
252
- .map((d) => d.sql);
266
+ });
253
267
  if (commandsToExecute.length) {
254
- const commandsToExecuteBySuperUser = commandsToExecute.filter((sql) => sql.startsWith("CREATE EXTENSION") || sql.startsWith("CREATE SCHEMA"));
255
- const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !commandsToExecuteBySuperUser.includes(sql));
268
+ const commandsToExecuteBySuperUser = commandsToExecute.filter(requireSuperUser);
269
+ const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !requireSuperUser(sql));
256
270
  const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
257
271
  if (!userIsCSQLAdmin && commandsToExecuteBySuperUser.length) {
258
272
  throw new error_1.FirebaseError(`Some SQL commands required for this migration require Admin permissions.\n
259
273
  Please ask a user with 'roles/cloudsql.admin' to apply the following commands.\n
260
- ${commandsToExecuteBySuperUser.join("\n")}`);
274
+ ${diffsToString(commandsToExecuteBySuperUser)}`);
261
275
  }
262
276
  const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
263
277
  if (schemaInfo.setupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField) {
@@ -266,56 +280,67 @@ async function handleIncompatibleSchemaError(args) {
266
280
  `If you would like Data Connect to manage your database schema, run 'firebase dataconnect:sql:setup'`);
267
281
  }
268
282
  if (!(await (0, permissionsSetup_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId), (await (0, connect_1.getIAMUser)(options)).user))) {
269
- throw new error_1.FirebaseError(`Command aborted. Only users granted firebaseowner SQL role can run migrations.`);
283
+ if (!userIsCSQLAdmin) {
284
+ throw new error_1.FirebaseError(`Command aborted. Only users granted firebaseowner SQL role can run migrations.`);
285
+ }
286
+ const account = (await (0, requireAuth_1.requireAuth)(options));
287
+ (0, utils_1.logLabeledBullet)("dataconnect", `Granting firebaseowner role to myself ${account}...`);
288
+ await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, "owner", account);
270
289
  }
271
290
  if (commandsToExecuteBySuperUser.length) {
272
- logger_1.logger.info(`The diffs require CloudSQL superuser permissions, attempting to apply changes as superuser.`);
273
- await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, commandsToExecuteBySuperUser, false);
291
+ (0, utils_1.logLabeledBullet)("dataconnect", `Executing admin SQL commands as superuser...`);
292
+ await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, commandsToExecuteBySuperUser.map((d) => d.sql), false);
274
293
  }
275
294
  if (commandsToExecuteByOwner.length) {
276
- await (0, connect_1.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [`SET ROLE "${(0, permissions_1.firebaseowner)(databaseId)}"`, ...commandsToExecuteByOwner], false);
295
+ await (0, connect_1.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [`SET ROLE "${(0, permissions_1.firebaseowner)(databaseId)}"`, ...commandsToExecuteByOwner.map((d) => d.sql)], false);
277
296
  return incompatibleSchemaError.diffs;
278
297
  }
279
298
  }
280
299
  return [];
281
300
  }
282
- async function promptForSchemaMigration(options, instanceName, databaseId, err, validateOnly, validationMode) {
301
+ async function promptForSchemaMigration(options, instanceId, databaseId, err, validateOnly, validationMode) {
283
302
  if (!err) {
284
303
  return "none";
285
304
  }
286
- if (validationMode === "STRICT_AFTER_COMPATIBLE" && (options.nonInteractive || options.force)) {
287
- return "none";
288
- }
289
- displaySchemaChanges(err, validationMode, instanceName, databaseId);
305
+ const defaultChoice = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
306
+ displaySchemaChanges(err, validationMode);
290
307
  if (!options.nonInteractive) {
291
308
  if (validateOnly && options.force) {
292
- return "all";
309
+ return defaultChoice;
310
+ }
311
+ let choices = [
312
+ { name: "Execute all", value: "all" },
313
+ ];
314
+ if (err.destructive) {
315
+ choices = [{ name: `Execute all ${clc.red("(including destructive)")}`, value: "all" }];
316
+ if (err.diffs.some((d) => !d.destructive)) {
317
+ choices.push({ name: "Execute safe only", value: "safe" });
318
+ }
293
319
  }
294
- const message = validationMode === "STRICT_AFTER_COMPATIBLE"
295
- ? `Would you like to execute these optional changes against ${databaseId} in your CloudSQL instance ${instanceName}?`
296
- : `Would you like to execute these changes against ${databaseId} in your CloudSQL instance ${instanceName}?`;
297
- let executeChangePrompt = "Execute changes";
298
320
  if (validationMode === "STRICT_AFTER_COMPATIBLE") {
299
- executeChangePrompt = "Execute optional changes";
321
+ choices.push({ name: "Skip them", value: "none" });
300
322
  }
301
- if (err.destructive) {
302
- executeChangePrompt = executeChangePrompt + " (including destructive changes)";
323
+ else {
324
+ choices.push({ name: "Abort", value: "abort" });
303
325
  }
304
- const choices = [
305
- { name: executeChangePrompt, value: "all" },
306
- { name: "Abort changes", value: "none" },
307
- ];
308
- const defaultValue = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
309
- return await (0, prompt_1.select)({ message, choices, default: defaultValue });
326
+ const ans = await (0, prompt_1.select)({
327
+ message: `Do you want to execute these SQL against ${instanceId}:${databaseId}?`,
328
+ choices: choices,
329
+ default: defaultChoice,
330
+ });
331
+ if (ans === "abort") {
332
+ throw new error_1.FirebaseError("Command aborted.");
333
+ }
334
+ return ans;
310
335
  }
311
336
  if (!validateOnly) {
312
337
  throw new error_1.FirebaseError("Command aborted. Your database schema is incompatible with your Data Connect schema. Run `firebase dataconnect:sql:migrate` to migrate your database schema");
313
338
  }
314
339
  else if (options.force) {
315
- return "all";
340
+ return defaultChoice;
316
341
  }
317
342
  else if (!err.destructive) {
318
- return "all";
343
+ return defaultChoice;
319
344
  }
320
345
  else {
321
346
  throw new error_1.FirebaseError("Command aborted. This schema migration includes potentially destructive changes. If you'd like to execute it anyway, rerun this command with --force");
@@ -344,17 +369,27 @@ async function deleteInvalidConnectors(invalidConnectors) {
344
369
  }
345
370
  function displayInvalidConnectors(invalidConnectors) {
346
371
  const connectorIds = invalidConnectors.map((i) => i.split("/").pop()).join(", ");
347
- (0, utils_1.logLabeledWarning)("dataconnect", `The schema you are deploying is incompatible with the following existing connectors: ${connectorIds}.`);
372
+ (0, utils_1.logLabeledWarning)("dataconnect", `The schema you are deploying is incompatible with the following existing connectors: ${clc.bold(connectorIds)}.`);
348
373
  (0, utils_1.logLabeledWarning)("dataconnect", `This is a ${clc.red("breaking")} change and may break existing apps.`);
349
374
  }
350
- async function ensureServiceIsConnectedToCloudSql(serviceName, instanceId, databaseId, linkIfNotConnected) {
375
+ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, linkIfNotConnected) {
376
+ var _a, _b, _c, _d;
351
377
  let currentSchema = await (0, client_1.getSchema)(serviceName);
352
- if (!currentSchema) {
378
+ let postgresql = (_b = (_a = currentSchema === null || currentSchema === void 0 ? void 0 : currentSchema.datasources) === null || _a === void 0 ? void 0 : _a.find((d) => d.postgresql)) === null || _b === void 0 ? void 0 : _b.postgresql;
379
+ if ((currentSchema === null || currentSchema === void 0 ? void 0 : currentSchema.reconciling) &&
380
+ (postgresql === null || postgresql === void 0 ? void 0 : postgresql.ephemeral) &&
381
+ ((_c = postgresql === null || postgresql === void 0 ? void 0 : postgresql.cloudSql) === null || _c === void 0 ? void 0 : _c.instance) &&
382
+ (postgresql === null || postgresql === void 0 ? void 0 : postgresql.schemaValidation) === "NONE") {
383
+ const [, , , , , serviceId] = serviceName.split("/");
384
+ const [, projectId, , , , instanceId] = postgresql.cloudSql.instance.split("/");
385
+ throw new error_1.FirebaseError(`While checking the service ${serviceId}, ` + (0, provisionCloudSql_1.cloudSQLBeingCreated)(projectId, instanceId));
386
+ }
387
+ if (!currentSchema || !postgresql) {
353
388
  if (!linkIfNotConnected) {
354
389
  (0, utils_1.logLabeledWarning)("dataconnect", `Not yet linked to the Cloud SQL instance.`);
355
390
  return;
356
391
  }
357
- (0, utils_1.logLabeledBullet)("dataconnect", `linking the Cloud SQL instance...`);
392
+ (0, utils_1.logLabeledBullet)("dataconnect", `Linking the Cloud SQL instance...`);
358
393
  currentSchema = {
359
394
  name: `${serviceName}/schemas/${types_1.SCHEMA_ID}`,
360
395
  source: {
@@ -362,84 +397,95 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceId, datab
362
397
  },
363
398
  datasources: [
364
399
  {
365
- postgresql: {
366
- database: databaseId,
367
- schemaValidation: "NONE",
368
- cloudSql: {
369
- instance: instanceId,
370
- },
371
- },
400
+ postgresql: { ephemeral: true },
372
401
  },
373
402
  ],
374
403
  };
375
404
  }
376
- const postgresDatasource = currentSchema.datasources.find((d) => d.postgresql);
377
- const postgresql = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql;
378
- if ((postgresql === null || postgresql === void 0 ? void 0 : postgresql.cloudSql.instance) !== instanceId) {
379
- (0, utils_1.logLabeledWarning)("dataconnect", `Switching connected Cloud SQL instance\nFrom ${postgresql === null || postgresql === void 0 ? void 0 : postgresql.cloudSql.instance}\nTo ${instanceId}`);
405
+ if (!postgresql) {
406
+ postgresql = currentSchema.datasources[0].postgresql;
407
+ }
408
+ let alreadyConnected = !postgresql.ephemeral || false;
409
+ if (((_d = postgresql.cloudSql) === null || _d === void 0 ? void 0 : _d.instance) && postgresql.cloudSql.instance !== instanceName) {
410
+ alreadyConnected = false;
411
+ (0, utils_1.logLabeledWarning)("dataconnect", `Switching connected Cloud SQL instance\n From ${postgresql.cloudSql.instance}\n To ${instanceName}`);
380
412
  }
381
- if ((postgresql === null || postgresql === void 0 ? void 0 : postgresql.database) !== databaseId) {
382
- (0, utils_1.logLabeledWarning)("dataconnect", `Switching connected Postgres database from ${postgresql === null || postgresql === void 0 ? void 0 : postgresql.database} to ${databaseId}`);
413
+ if (postgresql.database && postgresql.database !== databaseId) {
414
+ alreadyConnected = false;
415
+ (0, utils_1.logLabeledWarning)("dataconnect", `Switching connected Postgres database from ${postgresql.database} to ${databaseId}`);
383
416
  }
384
- if (!postgresql || postgresql.schemaValidation !== "NONE") {
417
+ if (alreadyConnected) {
385
418
  return;
386
419
  }
387
- postgresql.schemaValidation = "STRICT";
388
420
  try {
421
+ postgresql.schemaValidation = "STRICT";
422
+ postgresql.database = databaseId;
423
+ postgresql.cloudSql = { instance: instanceName };
389
424
  await (0, client_1.upsertSchema)(currentSchema, false);
390
425
  }
391
426
  catch (err) {
392
427
  if ((err === null || err === void 0 ? void 0 : err.status) >= 500) {
393
428
  throw err;
394
429
  }
395
- logger_1.logger.debug(err);
430
+ logger_1.logger.debug(`Failed to ensure service is connected to Cloud SQL: ${err.message}`);
396
431
  }
397
432
  }
398
433
  exports.ensureServiceIsConnectedToCloudSql = ensureServiceIsConnectedToCloudSql;
399
- function displaySchemaChanges(error, validationMode, instanceName, databaseId) {
434
+ function displayStartSchemaDiff(validationMode) {
435
+ switch (validationMode) {
436
+ case "COMPATIBLE":
437
+ (0, utils_1.logLabeledBullet)("dataconnect", `Generating SQL schema migrations to be compatible...`);
438
+ break;
439
+ case "STRICT":
440
+ (0, utils_1.logLabeledBullet)("dataconnect", `Generating SQL schema migrations to match exactly...`);
441
+ break;
442
+ }
443
+ }
444
+ function displayNoSchemaDiff(instanceId, databaseId, validationMode) {
445
+ switch (validationMode) {
446
+ case "COMPATIBLE":
447
+ (0, utils_1.logLabeledSuccess)("dataconnect", `Database schema of ${instanceId}:${databaseId} is compatible with Data Connect Schema.`);
448
+ break;
449
+ case "STRICT":
450
+ (0, utils_1.logLabeledSuccess)("dataconnect", `Database schema of ${instanceId}:${databaseId} matches Data Connect Schema exactly.`);
451
+ break;
452
+ }
453
+ }
454
+ function displaySchemaChanges(error, validationMode) {
400
455
  switch (error.violationType) {
401
456
  case "INCOMPATIBLE_SCHEMA":
402
457
  {
403
- let message;
404
- if (validationMode === "COMPATIBLE") {
405
- message =
406
- "Your PostgreSQL database " +
407
- databaseId +
408
- " in your CloudSQL instance " +
409
- instanceName +
410
- " must be migrated in order to be compatible with your application schema. " +
411
- "The following SQL statements will migrate your database schema to be compatible with your new Data Connect schema.\n" +
412
- error.diffs.map(toString).join("\n");
413
- }
414
- else if (validationMode === "STRICT_AFTER_COMPATIBLE") {
415
- message =
416
- "Your new application schema is compatible with the schema of your PostgreSQL database " +
417
- databaseId +
418
- " in your CloudSQL instance " +
419
- instanceName +
420
- ", but contains unused tables or columns. " +
421
- "The following optional SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
422
- error.diffs.map(toString).join("\n");
423
- }
424
- else {
425
- message =
426
- "Your PostgreSQL database " +
427
- databaseId +
428
- " in your CloudSQL instance " +
429
- instanceName +
430
- " must be migrated in order to match your application schema. " +
431
- "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
432
- error.diffs.map(toString).join("\n");
458
+ switch (validationMode) {
459
+ case "COMPATIBLE":
460
+ (0, utils_1.logLabeledWarning)("dataconnect", `PostgreSQL schema is incompatible with the Data Connect Schema.
461
+ Those SQL statements will migrate it to be compatible:
462
+
463
+ ${diffsToString(error.diffs)}
464
+ `);
465
+ break;
466
+ case "STRICT_AFTER_COMPATIBLE":
467
+ (0, utils_1.logLabeledBullet)("dataconnect", `PostgreSQL schema contains unused SQL objects not part of the Data Connect Schema.
468
+ Those SQL statements will migrate it to match exactly:
469
+
470
+ ${diffsToString(error.diffs)}
471
+ `);
472
+ break;
473
+ case "STRICT":
474
+ (0, utils_1.logLabeledWarning)("dataconnect", `PostgreSQL schema does not match the Data Connect Schema.
475
+ Those SQL statements will migrate it to match exactly:
476
+
477
+ ${diffsToString(error.diffs)}
478
+ `);
479
+ break;
433
480
  }
434
- (0, utils_1.logLabeledWarning)("dataconnect", message);
435
481
  }
436
482
  break;
437
483
  case "INACCESSIBLE_SCHEMA":
438
484
  {
439
- const message = "Cannot access your CloudSQL database to validate schema. " +
440
- "The following SQL statements can setup a new database schema.\n" +
441
- error.diffs.map(toString).join("\n");
442
- (0, utils_1.logLabeledWarning)("dataconnect", message);
485
+ (0, utils_1.logLabeledWarning)("dataconnect", `Cannot access CloudSQL database to validate schema.
486
+ Here is the complete expected SQL schema:
487
+ ${diffsToString(error.diffs)}
488
+ `);
443
489
  (0, utils_1.logLabeledWarning)("dataconnect", "Some SQL resources may already exist.");
444
490
  }
445
491
  break;
@@ -447,6 +493,12 @@ function displaySchemaChanges(error, validationMode, instanceName, databaseId) {
447
493
  throw new error_1.FirebaseError(`Unknown schema violation type: ${error.violationType}, IncompatibleSqlSchemaError: ${error}`);
448
494
  }
449
495
  }
450
- function toString(diff) {
496
+ function requireSuperUser(diff) {
497
+ return diff.sql.startsWith("CREATE EXTENSION") || diff.sql.startsWith("CREATE SCHEMA");
498
+ }
499
+ function diffsToString(diffs) {
500
+ return diffs.map(diffToString).join("\n\n");
501
+ }
502
+ function diffToString(diff) {
451
503
  return `\/** ${diff.destructive ? clc.red("Destructive: ") : ""}${diff.description}*\/\n${(0, sql_formatter_1.format)(diff.sql, { language: "postgresql" })}`;
452
504
  }
@@ -33,12 +33,12 @@ async function default_1(context, options) {
33
33
  utils.logLabeledSuccess("dataconnect", `Created service ${s.serviceName}`);
34
34
  }));
35
35
  if (servicesToDelete.length) {
36
+ const serviceToDeleteList = servicesToDelete.map((s) => " - " + s.name).join("\n");
36
37
  if (await (0, prompt_1.confirm)({
37
- force: options.force,
38
+ force: false,
38
39
  nonInteractive: options.nonInteractive,
39
- message: `The following services exist on ${projectId} but are not listed in your 'firebase.json'\n${servicesToDelete
40
- .map((s) => s.name)
41
- .join("\n")}\nWould you like to delete these services?`,
40
+ message: `The following services exist on ${projectId} but are not listed in your 'firebase.json'\n${serviceToDeleteList}\nWould you like to delete these services?`,
41
+ default: false,
42
42
  })) {
43
43
  await Promise.all(servicesToDelete.map(async (s) => {
44
44
  await client.deleteService(s.name);
@@ -52,22 +52,20 @@ async function default_1(context, options) {
52
52
  return !filters || (filters === null || filters === void 0 ? void 0 : filters.some((f) => si.dataConnectYaml.serviceId === f.serviceId));
53
53
  })
54
54
  .map(async (s) => {
55
- var _a, _b;
55
+ var _a, _b, _c;
56
56
  const postgresDatasource = s.schema.datasources.find((d) => d.postgresql);
57
57
  if (postgresDatasource) {
58
- const instanceId = (_a = postgresDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instance.split("/").pop();
59
- const databaseId = (_b = postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.database;
58
+ const instanceId = (_b = (_a = postgresDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql) === null || _b === void 0 ? void 0 : _b.instance.split("/").pop();
59
+ const databaseId = (_c = postgresDatasource.postgresql) === null || _c === void 0 ? void 0 : _c.database;
60
60
  if (!instanceId || !databaseId) {
61
61
  return Promise.resolve();
62
62
  }
63
- const enableGoogleMlIntegration = (0, types_1.requiresVector)(s.deploymentMetadata);
64
- return (0, provisionCloudSql_1.provisionCloudSql)({
63
+ return (0, provisionCloudSql_1.setupCloudSql)({
65
64
  projectId,
66
65
  location: (0, names_1.parseServiceName)(s.serviceName).location,
67
66
  instanceId,
68
67
  databaseId,
69
- enableGoogleMlIntegration,
70
- waitForCreation: true,
68
+ requireGoogleMlIntegration: (0, types_1.requiresVector)(s.deploymentMetadata),
71
69
  });
72
70
  }
73
71
  }));