firebase-tools 14.13.0 → 14.15.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 (76) hide show
  1. package/lib/apphosting/backend.js +40 -15
  2. package/lib/auth.js +37 -2
  3. package/lib/commands/apphosting-backends-create.js +8 -5
  4. package/lib/commands/dataconnect-sdk-generate.js +3 -5
  5. package/lib/commands/dataconnect-sql-diff.js +2 -2
  6. package/lib/commands/dataconnect-sql-grant.js +2 -2
  7. package/lib/commands/dataconnect-sql-migrate.js +2 -2
  8. package/lib/commands/dataconnect-sql-setup.js +2 -2
  9. package/lib/commands/dataconnect-sql-shell.js +2 -2
  10. package/lib/commands/init.js +3 -0
  11. package/lib/commands/login.js +12 -7
  12. package/lib/crashlytics/addNote.js +27 -0
  13. package/lib/crashlytics/deleteNote.js +23 -0
  14. package/lib/crashlytics/getIssueDetails.js +5 -20
  15. package/lib/crashlytics/getSampleCrash.js +6 -20
  16. package/lib/crashlytics/listNotes.js +29 -0
  17. package/lib/crashlytics/listTopDevices.js +33 -0
  18. package/lib/crashlytics/listTopIssues.js +8 -23
  19. package/lib/crashlytics/listTopOperatingSystems.js +32 -0
  20. package/lib/crashlytics/listTopVersions.js +32 -0
  21. package/lib/crashlytics/updateIssue.js +35 -0
  22. package/lib/crashlytics/utils.js +38 -0
  23. package/lib/dataconnect/appFinder.js +103 -0
  24. package/lib/dataconnect/errors.js +7 -1
  25. package/lib/dataconnect/load.js +105 -6
  26. package/lib/dataconnect/schemaMigration.js +152 -128
  27. package/lib/deploy/dataconnect/prepare.js +1 -3
  28. package/lib/deploy/dataconnect/release.js +33 -27
  29. package/lib/deploy/functions/prepare.js +2 -0
  30. package/lib/emulator/controller.js +7 -4
  31. package/lib/emulator/downloadableEmulatorInfo.json +17 -17
  32. package/lib/emulator/functionsEmulator.js +2 -0
  33. package/lib/functions/env.js +19 -15
  34. package/lib/gcp/cloudsql/connect.js +2 -1
  35. package/lib/gcp/cloudsql/permissionsSetup.js +10 -1
  36. package/lib/init/features/dataconnect/create_app.js +48 -0
  37. package/lib/init/features/dataconnect/index.js +20 -31
  38. package/lib/init/features/dataconnect/sdk.js +218 -161
  39. package/lib/init/features/index.js +3 -2
  40. package/lib/init/index.js +5 -1
  41. package/lib/management/apps.js +3 -3
  42. package/lib/mcp/prompts/core/deploy.js +51 -8
  43. package/lib/mcp/prompts/crashlytics/common.js +10 -0
  44. package/lib/mcp/prompts/crashlytics/fix_issue.js +89 -0
  45. package/lib/mcp/prompts/crashlytics/index.js +6 -0
  46. package/lib/mcp/prompts/crashlytics/prioritize_issues.js +79 -0
  47. package/lib/mcp/prompts/index.js +3 -2
  48. package/lib/mcp/tools/core/init.js +5 -1
  49. package/lib/mcp/tools/crashlytics/add_note.js +32 -0
  50. package/lib/mcp/tools/crashlytics/constants.js +11 -0
  51. package/lib/mcp/tools/crashlytics/delete_note.js +35 -0
  52. package/lib/mcp/tools/crashlytics/get_issue_details.js +2 -4
  53. package/lib/mcp/tools/crashlytics/get_sample_crash.js +4 -4
  54. package/lib/mcp/tools/crashlytics/index.js +16 -2
  55. package/lib/mcp/tools/crashlytics/list_notes.js +37 -0
  56. package/lib/mcp/tools/crashlytics/list_top_devices.js +33 -0
  57. package/lib/mcp/tools/crashlytics/list_top_issues.js +5 -7
  58. package/lib/mcp/tools/crashlytics/list_top_operating_systems.js +33 -0
  59. package/lib/mcp/tools/crashlytics/list_top_versions.js +33 -0
  60. package/lib/mcp/tools/crashlytics/update_issue.js +37 -0
  61. package/lib/mcp/tools/dataconnect/execute_graphql.js +2 -2
  62. package/lib/mcp/tools/dataconnect/execute_graphql_read.js +2 -2
  63. package/lib/mcp/tools/dataconnect/execute_mutation.js +2 -2
  64. package/lib/mcp/tools/dataconnect/execute_query.js +2 -2
  65. package/lib/mcp/tools/dataconnect/generate_operation.js +2 -2
  66. package/lib/mcp/tools/dataconnect/get_connector.js +2 -2
  67. package/lib/mcp/tools/dataconnect/get_schema.js +2 -2
  68. package/lib/utils.js +11 -1
  69. package/package.json +1 -1
  70. package/schema/firebase-config.json +3 -0
  71. package/templates/init/dataconnect/connector.yaml +0 -16
  72. package/templates/init/dataconnect/dataconnect.yaml +2 -1
  73. package/templates/init/dataconnect/mutations.gql +29 -29
  74. package/templates/init/dataconnect/queries.gql +73 -73
  75. package/templates/init/dataconnect/schema.gql +48 -48
  76. package/lib/dataconnect/fileUtils.js +0 -168
@@ -17,6 +17,7 @@ const cloudsqladmin_1 = require("../gcp/cloudsql/cloudsqladmin");
17
17
  const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
18
18
  const errors = require("./errors");
19
19
  const provisionCloudSql_1 = require("./provisionCloudSql");
20
+ const requireAuth_1 = require("../requireAuth");
20
21
  async function setupSchemaIfNecessary(instanceId, databaseId, options) {
21
22
  try {
22
23
  await (0, connect_1.setupIAMUsers)(instanceId, options);
@@ -24,87 +25,89 @@ async function setupSchemaIfNecessary(instanceId, databaseId, options) {
24
25
  switch (schemaInfo.setupStatus) {
25
26
  case permissionsSetup_1.SchemaSetupStatus.BrownField:
26
27
  case permissionsSetup_1.SchemaSetupStatus.GreenField:
28
+ logger_1.logger.debug(`Cloud SQL Database ${instanceId}:${databaseId} is already set up in ${schemaInfo.setupStatus}`);
27
29
  return schemaInfo.setupStatus;
28
30
  case permissionsSetup_1.SchemaSetupStatus.NotSetup:
29
31
  case permissionsSetup_1.SchemaSetupStatus.NotFound:
32
+ (0, utils_1.logLabeledBullet)("dataconnect", "Setting up Cloud SQL Database SQL permissions...");
30
33
  return await (0, permissionsSetup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options, true);
31
34
  default:
32
35
  throw new error_1.FirebaseError(`Unexpected schema setup status: ${schemaInfo.setupStatus}`);
33
36
  }
34
37
  }
35
38
  catch (err) {
36
- throw new error_1.FirebaseError(`Cannot setup SQL schema permissions of ${instanceId}:${databaseId}\n${err}`);
39
+ throw new error_1.FirebaseError(`Cannot setup Postgres SQL permissions of Cloud SQL database ${instanceId}:${databaseId}\n${err}`);
37
40
  }
38
41
  }
39
42
  async function diffSchema(options, schema, schemaValidation) {
40
- (0, utils_1.logLabeledBullet)("dataconnect", `Generating SQL schema migrations...`);
43
+ let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "STRICT";
44
+ setSchemaValidationMode(schema, validationMode);
45
+ displayStartSchemaDiff(validationMode);
41
46
  const { serviceName, instanceName, databaseId, instanceId } = getIdentifiers(schema);
42
47
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
43
- let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
44
- setSchemaValidationMode(schema, validationMode);
45
- let diffs = [];
48
+ let incompatible = undefined;
46
49
  try {
47
50
  await (0, client_1.upsertSchema)(schema, true);
48
- if (validationMode === "STRICT") {
49
- (0, utils_1.logLabeledSuccess)("dataconnect", `database schema of ${instanceId}:${databaseId} is up to date.`);
50
- }
51
- else {
52
- (0, utils_1.logLabeledSuccess)("dataconnect", `database schema of ${instanceId}:${databaseId} is compatible.`);
53
- }
51
+ displayNoSchemaDiff(instanceId, databaseId, validationMode);
54
52
  }
55
53
  catch (err) {
56
54
  if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
57
55
  throw err;
58
56
  }
57
+ incompatible = errors.getIncompatibleSchemaError(err);
59
58
  const invalidConnectors = errors.getInvalidConnectors(err);
60
- const incompatible = errors.getIncompatibleSchemaError(err);
61
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
+ }
62
64
  throw err;
63
65
  }
64
66
  if (invalidConnectors.length) {
65
67
  displayInvalidConnectors(invalidConnectors);
66
68
  }
67
- if (incompatible) {
68
- displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
69
- diffs = incompatible.diffs;
70
- }
71
69
  }
72
- if (!schemaValidation) {
73
- validationMode = "STRICT";
74
- setSchemaValidationMode(schema, validationMode);
75
- try {
76
- (0, utils_1.logLabeledBullet)("dataconnect", `generating schema changes, including optional changes...`);
77
- await (0, client_1.upsertSchema)(schema, true);
78
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
79
- }
80
- catch (err) {
81
- if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
82
- throw err;
83
- }
84
- const incompatible = errors.getIncompatibleSchemaError(err);
85
- if (incompatible) {
86
- if (!diffsEqual(diffs, incompatible.diffs)) {
87
- if (diffs.length === 0) {
88
- displaySchemaChanges(incompatible, "STRICT_AFTER_COMPATIBLE", instanceName, databaseId);
89
- }
90
- else {
91
- displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
92
- }
93
- diffs = incompatible.diffs;
94
- }
95
- else {
96
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
97
- }
98
- }
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;
99
89
  }
90
+ compatibleIncompatible = errors.getIncompatibleSchemaError(err);
100
91
  }
101
- 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;
102
103
  }
103
104
  exports.diffSchema = diffSchema;
104
105
  async function migrateSchema(args) {
105
106
  var _a;
106
- (0, utils_1.logLabeledBullet)("dataconnect", `Generating SQL schema migrations...`);
107
107
  const { options, schema, validateOnly, schemaValidation } = args;
108
+ let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
109
+ setSchemaValidationMode(schema, validationMode);
110
+ displayStartSchemaDiff(validationMode);
108
111
  const projectId = (0, projectUtils_1.needProjectId)(options);
109
112
  const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
110
113
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true);
@@ -124,12 +127,10 @@ async function migrateSchema(args) {
124
127
  return [];
125
128
  }
126
129
  await setupSchemaIfNecessary(instanceId, databaseId, options);
127
- let validationMode = schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE";
128
- setSchemaValidationMode(schema, validationMode);
129
130
  let diffs = [];
130
131
  try {
131
132
  await (0, client_1.upsertSchema)(schema, validateOnly);
132
- (0, utils_1.logLabeledSuccess)("dataconnect", `database schema of ${instanceId}:${databaseId} is up to date.`);
133
+ displayNoSchemaDiff(instanceId, databaseId, validationMode);
133
134
  }
134
135
  catch (err) {
135
136
  if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
@@ -138,9 +139,13 @@ async function migrateSchema(args) {
138
139
  const incompatible = errors.getIncompatibleSchemaError(err);
139
140
  const invalidConnectors = errors.getInvalidConnectors(err);
140
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
+ }
141
146
  throw err;
142
147
  }
143
- const migrationMode = await promptForSchemaMigration(options, instanceName, databaseId, incompatible, validateOnly, validationMode);
148
+ const migrationMode = await promptForSchemaMigration(options, instanceId, databaseId, incompatible, validateOnly, validationMode);
144
149
  const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(options, serviceName, invalidConnectors, validateOnly);
145
150
  if (incompatible) {
146
151
  diffs = await handleIncompatibleSchemaError({
@@ -173,7 +178,7 @@ async function migrateSchema(args) {
173
178
  if (!incompatible && !invalidConnectors.length) {
174
179
  throw err;
175
180
  }
176
- 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");
177
182
  if (incompatible) {
178
183
  const maybeDiffs = await handleIncompatibleSchemaError({
179
184
  options,
@@ -193,16 +198,12 @@ async function grantRoleToUserInSchema(options, schema) {
193
198
  const role = options.role;
194
199
  const email = options.email;
195
200
  const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
196
- const projectId = (0, projectUtils_1.needProjectId)(options);
197
- const { user, mode } = (0, connect_1.toDatabaseUser)(email);
198
- const fdcSqlRole = permissionsSetup_1.fdcSqlRoleMap[role](databaseId);
199
201
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
200
202
  const schemaSetupStatus = await setupSchemaIfNecessary(instanceId, databaseId, options);
201
203
  if (schemaSetupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField && role === "owner") {
202
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'`);
203
205
  }
204
- await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
205
- await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
206
+ await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, role, email);
206
207
  }
207
208
  exports.grantRoleToUserInSchema = grantRoleToUserInSchema;
208
209
  function diffsEqual(x, y) {
@@ -253,11 +254,7 @@ function suggestedCommand(serviceName, invalidConnectorNames) {
253
254
  }
254
255
  async function handleIncompatibleSchemaError(args) {
255
256
  const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args;
256
- if (incompatibleSchemaError.destructive && choice === "safe") {
257
- 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");
258
- }
259
- const commandsToExecute = incompatibleSchemaError.diffs
260
- .filter((d) => {
257
+ const commandsToExecute = incompatibleSchemaError.diffs.filter((d) => {
261
258
  switch (choice) {
262
259
  case "all":
263
260
  return true;
@@ -266,16 +263,15 @@ async function handleIncompatibleSchemaError(args) {
266
263
  case "none":
267
264
  return false;
268
265
  }
269
- })
270
- .map((d) => d.sql);
266
+ });
271
267
  if (commandsToExecute.length) {
272
- const commandsToExecuteBySuperUser = commandsToExecute.filter((sql) => sql.startsWith("CREATE EXTENSION") || sql.startsWith("CREATE SCHEMA"));
273
- const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !commandsToExecuteBySuperUser.includes(sql));
268
+ const commandsToExecuteBySuperUser = commandsToExecute.filter(requireSuperUser);
269
+ const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !requireSuperUser(sql));
274
270
  const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
275
271
  if (!userIsCSQLAdmin && commandsToExecuteBySuperUser.length) {
276
272
  throw new error_1.FirebaseError(`Some SQL commands required for this migration require Admin permissions.\n
277
273
  Please ask a user with 'roles/cloudsql.admin' to apply the following commands.\n
278
- ${commandsToExecuteBySuperUser.join("\n")}`);
274
+ ${diffsToString(commandsToExecuteBySuperUser)}`);
279
275
  }
280
276
  const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
281
277
  if (schemaInfo.setupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField) {
@@ -284,56 +280,67 @@ async function handleIncompatibleSchemaError(args) {
284
280
  `If you would like Data Connect to manage your database schema, run 'firebase dataconnect:sql:setup'`);
285
281
  }
286
282
  if (!(await (0, permissionsSetup_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId), (await (0, connect_1.getIAMUser)(options)).user))) {
287
- 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);
288
289
  }
289
290
  if (commandsToExecuteBySuperUser.length) {
290
- logger_1.logger.info(`The diffs require CloudSQL superuser permissions, attempting to apply changes as superuser.`);
291
- 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);
292
293
  }
293
294
  if (commandsToExecuteByOwner.length) {
294
- 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);
295
296
  return incompatibleSchemaError.diffs;
296
297
  }
297
298
  }
298
299
  return [];
299
300
  }
300
- async function promptForSchemaMigration(options, instanceName, databaseId, err, validateOnly, validationMode) {
301
+ async function promptForSchemaMigration(options, instanceId, databaseId, err, validateOnly, validationMode) {
301
302
  if (!err) {
302
303
  return "none";
303
304
  }
304
- if (validationMode === "STRICT_AFTER_COMPATIBLE" && (options.nonInteractive || options.force)) {
305
- return "none";
306
- }
307
- displaySchemaChanges(err, validationMode, instanceName, databaseId);
305
+ const defaultChoice = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
306
+ displaySchemaChanges(err, validationMode);
308
307
  if (!options.nonInteractive) {
309
308
  if (validateOnly && options.force) {
310
- 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
+ }
311
319
  }
312
- const message = validationMode === "STRICT_AFTER_COMPATIBLE"
313
- ? `Would you like to execute these optional changes against ${databaseId} in your CloudSQL instance ${instanceName}?`
314
- : `Would you like to execute these changes against ${databaseId} in your CloudSQL instance ${instanceName}?`;
315
- let executeChangePrompt = "Execute changes";
316
320
  if (validationMode === "STRICT_AFTER_COMPATIBLE") {
317
- executeChangePrompt = "Execute optional changes";
321
+ choices.push({ name: "Skip them", value: "none" });
318
322
  }
319
- if (err.destructive) {
320
- executeChangePrompt = executeChangePrompt + " (including destructive changes)";
323
+ else {
324
+ choices.push({ name: "Abort", value: "abort" });
321
325
  }
322
- const choices = [
323
- { name: executeChangePrompt, value: "all" },
324
- { name: "Abort changes", value: "none" },
325
- ];
326
- const defaultValue = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
327
- 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;
328
335
  }
329
336
  if (!validateOnly) {
330
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");
331
338
  }
332
339
  else if (options.force) {
333
- return "all";
340
+ return defaultChoice;
334
341
  }
335
342
  else if (!err.destructive) {
336
- return "all";
343
+ return defaultChoice;
337
344
  }
338
345
  else {
339
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");
@@ -362,7 +369,7 @@ async function deleteInvalidConnectors(invalidConnectors) {
362
369
  }
363
370
  function displayInvalidConnectors(invalidConnectors) {
364
371
  const connectorIds = invalidConnectors.map((i) => i.split("/").pop()).join(", ");
365
- (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)}.`);
366
373
  (0, utils_1.logLabeledWarning)("dataconnect", `This is a ${clc.red("breaking")} change and may break existing apps.`);
367
374
  }
368
375
  async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, linkIfNotConnected) {
@@ -424,50 +431,61 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, dat
424
431
  }
425
432
  }
426
433
  exports.ensureServiceIsConnectedToCloudSql = ensureServiceIsConnectedToCloudSql;
427
- 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) {
428
455
  switch (error.violationType) {
429
456
  case "INCOMPATIBLE_SCHEMA":
430
457
  {
431
- let message;
432
- if (validationMode === "COMPATIBLE") {
433
- message =
434
- "Your PostgreSQL database " +
435
- databaseId +
436
- " in your CloudSQL instance " +
437
- instanceName +
438
- " must be migrated in order to be compatible with your application schema. " +
439
- "The following SQL statements will migrate your database schema to be compatible with your new Data Connect schema.\n" +
440
- 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;
441
480
  }
442
- else if (validationMode === "STRICT_AFTER_COMPATIBLE") {
443
- message =
444
- "Your new application schema is compatible with the schema of your PostgreSQL database " +
445
- databaseId +
446
- " in your CloudSQL instance " +
447
- instanceName +
448
- ", but contains unused tables or columns. " +
449
- "The following optional SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
450
- error.diffs.map(toString).join("\n");
451
- }
452
- else {
453
- message =
454
- "Your PostgreSQL database " +
455
- databaseId +
456
- " in your CloudSQL instance " +
457
- instanceName +
458
- " must be migrated in order to match your application schema. " +
459
- "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
460
- error.diffs.map(toString).join("\n");
461
- }
462
- (0, utils_1.logLabeledWarning)("dataconnect", message);
463
481
  }
464
482
  break;
465
483
  case "INACCESSIBLE_SCHEMA":
466
484
  {
467
- const message = "Cannot access your CloudSQL database to validate schema. " +
468
- "The following SQL statements can setup a new database schema.\n" +
469
- error.diffs.map(toString).join("\n");
470
- (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
+ `);
471
489
  (0, utils_1.logLabeledWarning)("dataconnect", "Some SQL resources may already exist.");
472
490
  }
473
491
  break;
@@ -475,6 +493,12 @@ function displaySchemaChanges(error, validationMode, instanceName, databaseId) {
475
493
  throw new error_1.FirebaseError(`Unknown schema violation type: ${error.violationType}, IncompatibleSqlSchemaError: ${error}`);
476
494
  }
477
495
  }
478
- 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) {
479
503
  return `\/** ${diff.destructive ? clc.red("Destructive: ") : ""}${diff.description}*\/\n${(0, sql_formatter_1.format)(diff.sql, { language: "postgresql" })}`;
480
504
  }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const clc = require("colorette");
4
4
  const load_1 = require("../../dataconnect/load");
5
- const fileUtils_1 = require("../../dataconnect/fileUtils");
6
5
  const logger_1 = require("../../logger");
7
6
  const utils = require("../../utils");
8
7
  const projectUtils_1 = require("../../projectUtils");
@@ -26,9 +25,8 @@ async function default_1(context, options) {
26
25
  }
27
26
  await (0, ensureApis_1.ensureApis)(projectId);
28
27
  await (0, requireTosAcceptance_1.requireTosAcceptance)(firedata_1.DATA_CONNECT_TOS_ID)(options);
29
- const serviceCfgs = (0, fileUtils_1.readFirebaseJson)(options.config);
30
28
  const filters = (0, filters_1.getResourceFilters)(options);
31
- const serviceInfos = await Promise.all(serviceCfgs.map((c) => (0, load_1.load)(projectId, options.config, c.source)));
29
+ const serviceInfos = await (0, load_1.loadAll)(projectId, options.config);
32
30
  for (const si of serviceInfos) {
33
31
  si.deploymentMetadata = await (0, build_1.build)(options, si.sourceDirectory, options.dryRun);
34
32
  }
@@ -6,6 +6,7 @@ const prompts_1 = require("../../dataconnect/prompts");
6
6
  const schemaMigration_1 = require("../../dataconnect/schemaMigration");
7
7
  const projectUtils_1 = require("../../projectUtils");
8
8
  const names_1 = require("../../dataconnect/names");
9
+ const logger_1 = require("../../logger");
9
10
  async function default_1(context, options) {
10
11
  const project = (0, projectUtils_1.needProjectId)(options);
11
12
  const serviceInfos = context.dataconnect.serviceInfos;
@@ -24,19 +25,7 @@ async function default_1(context, options) {
24
25
  validationMode: (_d = (_c = (_b = (_a = s.dataConnectYaml) === null || _a === void 0 ? void 0 : _a.schema) === null || _b === void 0 ? void 0 : _b.datasource) === null || _c === void 0 ? void 0 : _c.postgresql) === null || _d === void 0 ? void 0 : _d.schemaValidation,
25
26
  });
26
27
  });
27
- if (wantSchemas.length) {
28
- for (const s of wantSchemas) {
29
- await (0, schemaMigration_1.migrateSchema)({
30
- options,
31
- schema: s.schema,
32
- validateOnly: false,
33
- schemaValidation: s.validationMode,
34
- });
35
- utils.logLabeledSuccess("dataconnect", `Migrated schema ${s.schema.name}`);
36
- }
37
- }
38
- let wantConnectors = [];
39
- wantConnectors = wantConnectors.concat(...serviceInfos.map((si) => si.connectorInfo
28
+ const wantConnectors = serviceInfos.flatMap((si) => si.connectorInfo
40
29
  .filter((c) => {
41
30
  return (!filters ||
42
31
  filters.some((f) => {
@@ -44,22 +33,39 @@ async function default_1(context, options) {
44
33
  (f.connectorId === c.connectorYaml.connectorId || f.fullService));
45
34
  }));
46
35
  })
47
- .map((c) => c.connector)));
48
- const haveConnectors = await have(serviceInfos);
49
- const connectorsToDelete = filters
50
- ? []
51
- : haveConnectors.filter((h) => !wantConnectors.some((w) => w.name === h.name));
52
- if (wantConnectors.length) {
53
- await Promise.all(wantConnectors.map(async (c) => {
36
+ .map((c) => c.connector));
37
+ const remainingConnectors = await Promise.all(wantConnectors.map(async (c) => {
38
+ try {
54
39
  await (0, client_1.upsertConnector)(c);
55
- utils.logLabeledSuccess("dataconnect", `Deployed connector ${c.name}`);
56
- }));
57
- for (const c of connectorsToDelete) {
58
- await (0, prompts_1.promptDeleteConnector)(options, c.name);
59
40
  }
41
+ catch (err) {
42
+ logger_1.logger.debug("Error pre-deploying connector", c.name, err);
43
+ return c;
44
+ }
45
+ utils.logLabeledSuccess("dataconnect", `Deployed connector ${c.name}`);
46
+ return undefined;
47
+ }));
48
+ for (const s of wantSchemas) {
49
+ await (0, schemaMigration_1.migrateSchema)({
50
+ options,
51
+ schema: s.schema,
52
+ validateOnly: false,
53
+ schemaValidation: s.validationMode,
54
+ });
55
+ utils.logLabeledSuccess("dataconnect", `Migrated schema ${s.schema.name}`);
60
56
  }
61
- else {
62
- utils.logLabeledBullet("dataconnect", "No connectors to deploy.");
57
+ await Promise.all(remainingConnectors.map(async (c) => {
58
+ if (c) {
59
+ await (0, client_1.upsertConnector)(c);
60
+ utils.logLabeledSuccess("dataconnect", `Deployed connector ${c.name}`);
61
+ }
62
+ }));
63
+ const allConnectors = await deployedConnectors(serviceInfos);
64
+ const connectorsToDelete = filters
65
+ ? []
66
+ : allConnectors.filter((h) => !wantConnectors.some((w) => w.name === h.name));
67
+ for (const c of connectorsToDelete) {
68
+ await (0, prompts_1.promptDeleteConnector)(options, c.name);
63
69
  }
64
70
  let consolePath = "/dataconnect";
65
71
  if (serviceInfos.length === 1) {
@@ -73,7 +79,7 @@ async function default_1(context, options) {
73
79
  return;
74
80
  }
75
81
  exports.default = default_1;
76
- async function have(serviceInfos) {
82
+ async function deployedConnectors(serviceInfos) {
77
83
  let connectors = [];
78
84
  for (const si of serviceInfos) {
79
85
  connectors = connectors.concat(await (0, client_1.listConnectors)(si.serviceName));
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureAllRequiredAPIsEnabled = exports.warnIfNewGenkitFunctionIsMissingSecrets = exports.loadCodebases = exports.resolveCpuAndConcurrency = exports.inferBlockingDetails = exports.updateEndpointTargetedStatus = exports.inferDetailsFromExisting = exports.prepare = exports.EVENTARC_SOURCE_ENV = void 0;
4
4
  const clc = require("colorette");
5
+ const proto = require("../../gcp/proto");
5
6
  const backend = require("./backend");
6
7
  const build = require("./build");
7
8
  const ensureApiEnabled = require("../../ensureApiEnabled");
@@ -76,6 +77,7 @@ async function prepare(context, options, payload) {
76
77
  projectId: projectId,
77
78
  projectAlias: options.projectAlias,
78
79
  };
80
+ proto.convertIfPresent(userEnvOpt, config, "configDir", (cd) => options.config.path(cd));
79
81
  const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
80
82
  const envs = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
81
83
  const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend({
@@ -5,6 +5,7 @@ const clc = require("colorette");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
  const fsConfig = require("../firestore/fsConfig");
8
+ const proto = require("../gcp/proto");
8
9
  const logger_1 = require("../logger");
9
10
  const track_1 = require("../track");
10
11
  const utils = require("../utils");
@@ -43,7 +44,7 @@ const firestoreEmulator_1 = require("./firestoreEmulator");
43
44
  const hostingEmulator_1 = require("./hostingEmulator");
44
45
  const pubsubEmulator_1 = require("./pubsubEmulator");
45
46
  const storage_1 = require("./storage");
46
- const fileUtils_1 = require("../dataconnect/fileUtils");
47
+ const load_1 = require("../dataconnect/load");
47
48
  const tasksEmulator_1 = require("./tasksEmulator");
48
49
  const apphosting_1 = require("./apphosting");
49
50
  const webhook_1 = require("../dataconnect/webhook");
@@ -344,7 +345,7 @@ async function startAll(options, showUI = true, runningTestScript = false) {
344
345
  if (runtime && !(0, supported_1.isRuntime)(runtime)) {
345
346
  throw new error_1.FirebaseError(`Cannot load functions from ${functionsDir} because it has invalid runtime ${runtime}`);
346
347
  }
347
- emulatableBackends.push({
348
+ const backend = {
348
349
  functionsDir,
349
350
  runtime,
350
351
  codebase: cfg.codebase,
@@ -353,7 +354,9 @@ async function startAll(options, showUI = true, runningTestScript = false) {
353
354
  secretEnv: [],
354
355
  predefinedTriggers: options.extDevTriggers,
355
356
  ignore: cfg.ignore,
356
- });
357
+ };
358
+ proto.convertIfPresent(backend, cfg, "configDir", (cd) => path.join(projectDir, cd));
359
+ emulatableBackends.push(backend);
357
360
  }
358
361
  }
359
362
  if (extensionEmulator) {
@@ -556,7 +559,7 @@ async function startAll(options, showUI = true, runningTestScript = false) {
556
559
  await startEmulator(pubsubEmulator);
557
560
  }
558
561
  if (listenForEmulator.dataconnect) {
559
- const config = (0, fileUtils_1.readFirebaseJson)(options.config);
562
+ const config = (0, load_1.readFirebaseJson)(options.config);
560
563
  if (!config.length) {
561
564
  throw new error_1.FirebaseError("No Data Connect service found in firebase.json");
562
565
  }