firebase-tools 13.17.0 → 13.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/commands/dataconnect-services-list.js +4 -3
  2. package/lib/commands/dataconnect-sql-grant.js +37 -0
  3. package/lib/commands/dataconnect-sql-migrate.js +2 -1
  4. package/lib/commands/deploy.js +2 -0
  5. package/lib/commands/index.js +1 -0
  6. package/lib/dataconnect/client.js +1 -1
  7. package/lib/dataconnect/dataplaneClient.js +1 -1
  8. package/lib/dataconnect/ensureApis.js +7 -1
  9. package/lib/dataconnect/load.js +3 -1
  10. package/lib/dataconnect/provisionCloudSql.js +34 -21
  11. package/lib/dataconnect/schemaMigration.js +126 -73
  12. package/lib/deploy/dataconnect/deploy.js +16 -13
  13. package/lib/deploy/dataconnect/prepare.js +36 -0
  14. package/lib/deploy/dataconnect/release.js +9 -2
  15. package/lib/deploy/extensions/prepare.js +22 -9
  16. package/lib/deploy/firestore/prepare.js +10 -0
  17. package/lib/deploy/firestore/release.js +3 -6
  18. package/lib/deploy/functions/checkIam.js +7 -2
  19. package/lib/deploy/functions/ensure.js +10 -2
  20. package/lib/deploy/functions/prepare.js +2 -2
  21. package/lib/deploy/index.js +9 -5
  22. package/lib/emulator/downloadableEmulators.js +9 -9
  23. package/lib/firestore/checkDatabaseType.js +10 -3
  24. package/lib/frameworks/angular/index.js +15 -3
  25. package/lib/frameworks/next/utils.js +1 -1
  26. package/lib/gcp/cloudsql/permissions.js +6 -1
  27. package/lib/gcp/secretManager.js +12 -5
  28. package/lib/init/features/dataconnect/index.js +24 -11
  29. package/lib/init/features/firestore/index.js +20 -1
  30. package/lib/init/features/firestore/indexes.js +4 -4
  31. package/package.json +1 -1
  32. package/schema/connector-yaml.json +43 -17
  33. package/templates/init/dataconnect/connector.yaml +0 -1
  34. package/templates/init/dataconnect/dataconnect-fdccompatiblemode.yaml +12 -0
  35. package/templates/init/dataconnect/dataconnect.yaml +1 -1
@@ -36,14 +36,15 @@ exports.command = new command_1.Command("dataconnect:services:list")
36
36
  for (const service of services) {
37
37
  const schema = (_a = (await client.getSchema(service.name))) !== null && _a !== void 0 ? _a : {
38
38
  name: "",
39
- primaryDatasource: {},
39
+ datasources: [{}],
40
40
  source: { files: [] },
41
41
  };
42
42
  const connectors = await client.listConnectors(service.name);
43
43
  const serviceName = names.parseServiceName(service.name);
44
- const instanceName = (_c = (_b = schema === null || schema === void 0 ? void 0 : schema.primaryDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql.instance) !== null && _c !== void 0 ? _c : "";
44
+ const postgresDatasource = schema === null || schema === void 0 ? void 0 : schema.datasources.find((d) => d.postgresql);
45
+ const instanceName = (_c = (_b = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql.instance) !== null && _c !== void 0 ? _c : "";
45
46
  const instanceId = instanceName.split("/").pop();
46
- const dbId = (_e = (_d = schema === null || schema === void 0 ? void 0 : schema.primaryDatasource.postgresql) === null || _d === void 0 ? void 0 : _d.database) !== null && _e !== void 0 ? _e : "";
47
+ const dbId = (_e = (_d = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _d === void 0 ? void 0 : _d.database) !== null && _e !== void 0 ? _e : "";
47
48
  const dbName = `CloudSQL Instance: ${instanceId}\nDatabase: ${dbId}`;
48
49
  table.push([
49
50
  serviceName.serviceId,
@@ -0,0 +1,37 @@
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
+ const error_1 = require("../error");
12
+ const permissions_1 = require("../gcp/cloudsql/permissions");
13
+ const allowedRoles = Object.keys(permissions_1.fdcSqlRoleMap);
14
+ exports.command = new command_1.Command("dataconnect:sql:grant [serviceId]")
15
+ .description("Grants the SQL role <role> to the provided user or service account <email>.")
16
+ .option("-R, --role <role>", "The SQL role to grant. One of: owner, writer, or reader.")
17
+ .option("-E, --email <email>", "The email of the user or service account we would like to grant the role to.")
18
+ .before(requirePermissions_1.requirePermissions, ["firebasedataconnect.services.list"])
19
+ .before(requireAuth_1.requireAuth)
20
+ .action(async (serviceId, options) => {
21
+ const role = options.role;
22
+ const email = options.email;
23
+ if (!role) {
24
+ throw new error_1.FirebaseError("-R, --role <role> is required. Run the command with -h for more info.");
25
+ }
26
+ if (!email) {
27
+ throw new error_1.FirebaseError("-E, --email <email> is required. Run the command with -h for more info.");
28
+ }
29
+ if (!allowedRoles.includes(role.toLowerCase())) {
30
+ throw new error_1.FirebaseError(`Role should be one of ${allowedRoles.join(" | ")}.`);
31
+ }
32
+ const projectId = (0, projectUtils_1.needProjectId)(options);
33
+ await (0, ensureApis_1.ensureApis)(projectId);
34
+ const serviceInfo = await (0, fileUtils_1.pickService)(projectId, options.config, serviceId);
35
+ await (0, schemaMigration_1.grantRoleToUserInSchema)(options, serviceInfo.schema);
36
+ return { projectId, serviceId };
37
+ });
@@ -21,7 +21,7 @@ exports.command = new command_1.Command("dataconnect:sql:migrate [serviceId]")
21
21
  .before(requireAuth_1.requireAuth)
22
22
  .withForce("Execute any required database changes without prompting")
23
23
  .action(async (serviceId, options) => {
24
- var _a;
24
+ var _a, _b;
25
25
  const projectId = (0, projectUtils_1.needProjectId)(options);
26
26
  await (0, ensureApis_1.ensureApis)(projectId);
27
27
  const serviceInfo = await (0, fileUtils_1.pickService)(projectId, options.config, serviceId);
@@ -33,6 +33,7 @@ exports.command = new command_1.Command("dataconnect:sql:migrate [serviceId]")
33
33
  options,
34
34
  schema: serviceInfo.schema,
35
35
  validateOnly: true,
36
+ schemaValidation: (_b = serviceInfo.dataConnectYaml.schema.datasource.postgresql) === null || _b === void 0 ? void 0 : _b.schemaValidation,
36
37
  });
37
38
  if (diffs.length) {
38
39
  (0, utils_1.logLabeledSuccess)("dataconnect", `Database schema sucessfully migrated! Run 'firebase deploy' to deploy your new schema to your Data Connect service.`);
@@ -86,6 +86,8 @@ exports.command = new command_1.Command("deploy")
86
86
  "For data connect, can specify filters with colons to deploy only a service, connector, or schema" +
87
87
  '(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema"). ')
88
88
  .option("--except <targets>", 'deploy to all targets except specified (e.g. "database")')
89
+ .option("--dry-run", "Perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " +
90
+ "In order to provide better validation, this may still enable APIs on the target project.")
89
91
  .before(requireConfig_1.requireConfig)
90
92
  .before((options) => {
91
93
  options.filteredTargets = (0, filterTargets_1.filterTargets)(options, exports.VALID_DEPLOY_TARGETS);
@@ -207,6 +207,7 @@ function load(client) {
207
207
  client.dataconnect.sql = {};
208
208
  client.dataconnect.sql.diff = loadCommand("dataconnect-sql-diff");
209
209
  client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
210
+ client.dataconnect.sql.grant = loadCommand("dataconnect-sql-grant");
210
211
  client.dataconnect.sdk = {};
211
212
  client.dataconnect.sdk.generate = loadCommand("dataconnect-sdk-generate");
212
213
  client.target = loadCommand("target");
@@ -5,7 +5,7 @@ const api_1 = require("../api");
5
5
  const apiv2_1 = require("../apiv2");
6
6
  const operationPoller = require("../operation-poller");
7
7
  const types = require("./types");
8
- const DATACONNECT_API_VERSION = "v1alpha";
8
+ const DATACONNECT_API_VERSION = "v1beta";
9
9
  const PAGE_SIZE_MAX = 100;
10
10
  const dataconnectClient = () => new apiv2_1.Client({
11
11
  urlPrefix: (0, api_1.dataconnectOrigin)(),
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeGraphQL = void 0;
4
4
  const api_1 = require("../api");
5
5
  const apiv2_1 = require("../apiv2");
6
- const DATACONNECT_API_VERSION = "v1alpha";
6
+ const DATACONNECT_API_VERSION = "v1beta";
7
7
  const dataconnectDataplaneClient = () => new apiv2_1.Client({
8
8
  urlPrefix: (0, api_1.dataconnectOrigin)(),
9
9
  apiVersion: DATACONNECT_API_VERSION,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureApis = void 0;
3
+ exports.ensureSparkApis = exports.ensureApis = void 0;
4
4
  const api = require("../api");
5
5
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
6
6
  async function ensureApis(projectId) {
@@ -10,3 +10,9 @@ async function ensureApis(projectId) {
10
10
  await (0, ensureApiEnabled_1.ensure)(projectId, api.computeOrigin(), prefix);
11
11
  }
12
12
  exports.ensureApis = ensureApis;
13
+ async function ensureSparkApis(projectId) {
14
+ const prefix = "dataconnect";
15
+ await (0, ensureApiEnabled_1.ensure)(projectId, api.dataconnectOrigin(), prefix);
16
+ await (0, ensureApiEnabled_1.ensure)(projectId, api.cloudSQLAdminOrigin(), prefix);
17
+ }
18
+ exports.ensureSparkApis = ensureSparkApis;
@@ -30,7 +30,9 @@ async function load(projectId, config, sourceDirectory) {
30
30
  sourceDirectory: resolvedDir,
31
31
  schema: {
32
32
  name: `${serviceName}/schemas/${types_1.SCHEMA_ID}`,
33
- primaryDatasource: (0, types_1.toDatasource)(projectId, dataConnectYaml.location, dataConnectYaml.schema.datasource),
33
+ datasources: [
34
+ (0, types_1.toDatasource)(projectId, dataConnectYaml.location, dataConnectYaml.schema.datasource),
35
+ ],
34
36
  source: {
35
37
  files: schemaGQLs,
36
38
  },
@@ -11,19 +11,22 @@ const freeTrial_1 = require("./freeTrial");
11
11
  const error_1 = require("../error");
12
12
  async function provisionCloudSql(args) {
13
13
  let connectionName = "";
14
- const { projectId, locationId, instanceId, databaseId, enableGoogleMlIntegration, waitForCreation, silent, } = args;
14
+ const { projectId, locationId, instanceId, databaseId, enableGoogleMlIntegration, waitForCreation, silent, dryRun, } = args;
15
15
  try {
16
16
  const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
17
17
  silent || utils.logLabeledBullet("dataconnect", `Found existing instance ${instanceId}.`);
18
18
  connectionName = (existingInstance === null || existingInstance === void 0 ? void 0 : existingInstance.connectionName) || "";
19
19
  const why = getUpdateReason(existingInstance, enableGoogleMlIntegration);
20
20
  if (why) {
21
+ const cta = dryRun
22
+ ? `It will be updated on your next deploy.`
23
+ : `Updating instance. This may take a few minutes...`;
21
24
  silent ||
22
- utils.logLabeledBullet("dataconnect", `Instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
23
- `Updating instance. This may take a few minutes...` +
24
- why);
25
- await (0, utils_1.promiseWithSpinner)(() => cloudSqlAdminClient.updateInstanceForDataConnect(existingInstance, enableGoogleMlIntegration), "Updating your instance...");
26
- silent || utils.logLabeledBullet("dataconnect", "Instance updated");
25
+ utils.logLabeledBullet("dataconnect", `Instance ${instanceId} settings not compatible with Firebase Data Connect. ` + cta + why);
26
+ if (!dryRun) {
27
+ await (0, utils_1.promiseWithSpinner)(() => cloudSqlAdminClient.updateInstanceForDataConnect(existingInstance, enableGoogleMlIntegration), "Updating your instance...");
28
+ silent || utils.logLabeledBullet("dataconnect", "Instance updated");
29
+ }
27
30
  }
28
31
  }
29
32
  catch (err) {
@@ -35,19 +38,23 @@ async function provisionCloudSql(args) {
35
38
  (0, freeTrial_1.printFreeTrialUnavailable)(projectId, freeTrialInstanceId);
36
39
  throw new error_1.FirebaseError("Free trial unavailable.");
37
40
  }
41
+ const cta = dryRun ? "It will be created on your next deploy" : "Creating it now.";
38
42
  silent ||
39
- utils.logLabeledBullet("dataconnect", `CloudSQL instance '${instanceId}' not found, creating it.` +
43
+ utils.logLabeledBullet("dataconnect", `CloudSQL instance '${instanceId}' not found.` +
44
+ cta +
40
45
  `\nThis instance is provided under the terms of the Data Connect free trial ${(0, freeTrial_1.freeTrialTermsLink)()}` +
41
46
  `\nMonitor the progress at ${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}`);
42
- const newInstance = await (0, utils_1.promiseWithSpinner)(() => cloudSqlAdminClient.createInstance(projectId, locationId, instanceId, enableGoogleMlIntegration, waitForCreation), "Creating your instance...");
43
- if (newInstance) {
44
- silent || utils.logLabeledBullet("dataconnect", "Instance created");
45
- connectionName = (newInstance === null || newInstance === void 0 ? void 0 : newInstance.connectionName) || "";
46
- }
47
- else {
48
- silent ||
49
- utils.logLabeledBullet("dataconnect", "Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.");
50
- return connectionName;
47
+ if (!dryRun) {
48
+ const newInstance = await (0, utils_1.promiseWithSpinner)(() => cloudSqlAdminClient.createInstance(projectId, locationId, instanceId, enableGoogleMlIntegration, waitForCreation), "Creating your instance...");
49
+ if (newInstance) {
50
+ silent || utils.logLabeledBullet("dataconnect", "Instance created");
51
+ connectionName = (newInstance === null || newInstance === void 0 ? void 0 : newInstance.connectionName) || "";
52
+ }
53
+ else {
54
+ silent ||
55
+ utils.logLabeledBullet("dataconnect", "Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.");
56
+ return connectionName;
57
+ }
51
58
  }
52
59
  }
53
60
  try {
@@ -56,17 +63,23 @@ async function provisionCloudSql(args) {
56
63
  }
57
64
  catch (err) {
58
65
  if (err.status === 404) {
59
- silent ||
60
- utils.logLabeledBullet("dataconnect", `Database ${databaseId} not found, creating it now...`);
61
- await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
62
- silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
66
+ if (dryRun) {
67
+ silent ||
68
+ utils.logLabeledBullet("dataconnect", `Database ${databaseId} not found. It will be created on your next deploy.`);
69
+ }
70
+ else {
71
+ silent ||
72
+ utils.logLabeledBullet("dataconnect", `Database ${databaseId} not found, creating it now...`);
73
+ await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
74
+ silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
75
+ }
63
76
  }
64
77
  else {
65
78
  logger_1.logger.debug(`Unexpected error from CloudSQL: ${err}`);
66
79
  silent || utils.logLabeledWarning("dataconnect", `Database ${databaseId} is not accessible.`);
67
80
  }
68
81
  }
69
- if (enableGoogleMlIntegration) {
82
+ if (enableGoogleMlIntegration && !dryRun) {
70
83
  await (0, checkIam_1.grantRolesToCloudSqlServiceAccount)(projectId, instanceId, [GOOGLE_ML_INTEGRATION_ROLE]);
71
84
  }
72
85
  return connectionName;
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.migrateSchema = exports.diffSchema = void 0;
3
+ exports.grantRoleToUserInSchema = exports.migrateSchema = exports.diffSchema = void 0;
4
4
  const clc = require("colorette");
5
5
  const sql_formatter_1 = require("sql-formatter");
6
6
  const types_1 = require("./types");
7
7
  const client_1 = require("./client");
8
8
  const connect_1 = require("../gcp/cloudsql/connect");
9
9
  const permissions_1 = require("../gcp/cloudsql/permissions");
10
+ const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
11
+ const projectUtils_1 = require("../projectUtils");
10
12
  const prompt_1 = require("../prompt");
11
13
  const logger_1 = require("../logger");
12
14
  const error_1 = require("../error");
@@ -17,15 +19,9 @@ async function diffSchema(schema, schemaValidation) {
17
19
  const { serviceName, instanceName, databaseId } = getIdentifiers(schema);
18
20
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
19
21
  let diffs = [];
20
- let validationMode = "STRICT";
21
- if (experiments.isEnabled("fdccompatiblemode")) {
22
- if (!schemaValidation) {
23
- validationMode = "COMPATIBLE";
24
- }
25
- else {
26
- validationMode = schemaValidation;
27
- }
28
- }
22
+ let validationMode = experiments.isEnabled("fdccompatiblemode")
23
+ ? schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE"
24
+ : "STRICT";
29
25
  setSchemaValidationMode(schema, validationMode);
30
26
  try {
31
27
  if (!schemaValidation && experiments.isEnabled("fdccompatiblemode")) {
@@ -56,33 +52,31 @@ async function diffSchema(schema, schemaValidation) {
56
52
  diffs = incompatible.diffs;
57
53
  }
58
54
  }
59
- if (experiments.isEnabled("fdccompatiblemode")) {
60
- if (!schemaValidation) {
61
- validationMode = "STRICT";
62
- setSchemaValidationMode(schema, validationMode);
63
- try {
64
- (0, utils_1.logLabeledBullet)("dataconnect", `generating schema changes, including optional changes...`);
65
- await (0, client_1.upsertSchema)(schema, true);
66
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
55
+ if (experiments.isEnabled("fdccompatiblemode") && !schemaValidation) {
56
+ validationMode = "STRICT";
57
+ setSchemaValidationMode(schema, validationMode);
58
+ try {
59
+ (0, utils_1.logLabeledBullet)("dataconnect", `generating schema changes, including optional changes...`);
60
+ await (0, client_1.upsertSchema)(schema, true);
61
+ (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
62
+ }
63
+ catch (err) {
64
+ if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
65
+ throw err;
67
66
  }
68
- catch (err) {
69
- if ((err === null || err === void 0 ? void 0 : err.status) !== 400) {
70
- throw err;
71
- }
72
- const incompatible = errors.getIncompatibleSchemaError(err);
73
- if (incompatible) {
74
- if (!diffsEqual(diffs, incompatible.diffs)) {
75
- if (diffs.length === 0) {
76
- displaySchemaChanges(incompatible, "STRICT_AFTER_COMPATIBLE", instanceName, databaseId);
77
- }
78
- else {
79
- displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
80
- }
81
- diffs = incompatible.diffs;
67
+ const incompatible = errors.getIncompatibleSchemaError(err);
68
+ if (incompatible) {
69
+ if (!diffsEqual(diffs, incompatible.diffs)) {
70
+ if (diffs.length === 0) {
71
+ displaySchemaChanges(incompatible, "STRICT_AFTER_COMPATIBLE", instanceName, databaseId);
82
72
  }
83
73
  else {
84
- (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
74
+ displaySchemaChanges(incompatible, validationMode, instanceName, databaseId);
85
75
  }
76
+ diffs = incompatible.diffs;
77
+ }
78
+ else {
79
+ (0, utils_1.logLabeledSuccess)("dataconnect", `no additional optional changes`);
86
80
  }
87
81
  }
88
82
  }
@@ -91,10 +85,13 @@ async function diffSchema(schema, schemaValidation) {
91
85
  }
92
86
  exports.diffSchema = diffSchema;
93
87
  async function migrateSchema(args) {
94
- const { options, schema, validateOnly } = args;
88
+ const { options, schema, validateOnly, schemaValidation } = args;
95
89
  const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
96
90
  await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true);
97
- const validationMode = experiments.isEnabled("fdccompatiblemode") ? "COMPATIBLE" : "STRICT";
91
+ let diffs = [];
92
+ let validationMode = experiments.isEnabled("fdccompatiblemode")
93
+ ? schemaValidation !== null && schemaValidation !== void 0 ? schemaValidation : "COMPATIBLE"
94
+ : "STRICT";
98
95
  setSchemaValidationMode(schema, validationMode);
99
96
  try {
100
97
  await (0, client_1.upsertSchema)(schema, validateOnly);
@@ -111,7 +108,6 @@ async function migrateSchema(args) {
111
108
  }
112
109
  const migrationMode = await promptForSchemaMigration(options, instanceName, databaseId, incompatible, validateOnly, validationMode);
113
110
  const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(options, serviceName, invalidConnectors, validateOnly);
114
- let diffs = [];
115
111
  if (incompatible) {
116
112
  diffs = await handleIncompatibleSchemaError({
117
113
  options,
@@ -127,11 +123,54 @@ async function migrateSchema(args) {
127
123
  if (!validateOnly) {
128
124
  await (0, client_1.upsertSchema)(schema, validateOnly);
129
125
  }
130
- return diffs;
131
126
  }
132
- return [];
127
+ if (experiments.isEnabled("fdccompatiblemode") && !schemaValidation) {
128
+ validationMode = "STRICT";
129
+ setSchemaValidationMode(schema, validationMode);
130
+ try {
131
+ await (0, client_1.upsertSchema)(schema, validateOnly);
132
+ }
133
+ catch (err) {
134
+ if (err.status !== 400) {
135
+ throw err;
136
+ }
137
+ const incompatible = errors.getIncompatibleSchemaError(err);
138
+ const invalidConnectors = errors.getInvalidConnectors(err);
139
+ if (!incompatible && !invalidConnectors.length) {
140
+ throw err;
141
+ }
142
+ const migrationMode = await promptForSchemaMigration(options, instanceName, databaseId, incompatible, validateOnly, "STRICT_AFTER_COMPATIBLE");
143
+ if (incompatible) {
144
+ const maybeDiffs = await handleIncompatibleSchemaError({
145
+ options,
146
+ databaseId,
147
+ instanceId,
148
+ incompatibleSchemaError: incompatible,
149
+ choice: migrationMode,
150
+ });
151
+ diffs = diffs.concat(maybeDiffs);
152
+ }
153
+ }
154
+ }
155
+ return diffs;
133
156
  }
134
157
  exports.migrateSchema = migrateSchema;
158
+ async function grantRoleToUserInSchema(options, schema) {
159
+ const role = options.role;
160
+ const email = options.email;
161
+ const { instanceId, databaseId } = getIdentifiers(schema);
162
+ const projectId = (0, projectUtils_1.needProjectId)(options);
163
+ const { user, mode } = (0, connect_1.toDatabaseUser)(email);
164
+ const fdcSqlRole = permissions_1.fdcSqlRoleMap[role](databaseId);
165
+ const userIsCSQLAdmin = await (0, permissions_1.iamUserIsCSQLAdmin)(options);
166
+ if (!userIsCSQLAdmin) {
167
+ 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}`);
168
+ }
169
+ await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
170
+ await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
171
+ await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
172
+ }
173
+ exports.grantRoleToUserInSchema = grantRoleToUserInSchema;
135
174
  function diffsEqual(x, y) {
136
175
  if (x.length !== y.length) {
137
176
  return false;
@@ -146,17 +185,21 @@ function diffsEqual(x, y) {
146
185
  return true;
147
186
  }
148
187
  function setSchemaValidationMode(schema, schemaValidation) {
149
- if (experiments.isEnabled("fdccompatiblemode") && schema.primaryDatasource.postgresql) {
150
- schema.primaryDatasource.postgresql.schemaValidation = schemaValidation;
188
+ if (experiments.isEnabled("fdccompatiblemode")) {
189
+ const postgresDatasource = schema.datasources.find((d) => d.postgresql);
190
+ if (postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) {
191
+ postgresDatasource.postgresql.schemaValidation = schemaValidation;
192
+ }
151
193
  }
152
194
  }
153
195
  function getIdentifiers(schema) {
154
196
  var _a, _b;
155
- const databaseId = (_a = schema.primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.database;
197
+ const postgresDatasource = schema.datasources.find((d) => d.postgresql);
198
+ const databaseId = (_a = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.database;
156
199
  if (!databaseId) {
157
- throw new error_1.FirebaseError("Schema is missing primaryDatasource.postgresql?.database, cannot migrate");
200
+ throw new error_1.FirebaseError("Service does not have a postgres datasource, cannot migrate");
158
201
  }
159
- const instanceName = (_b = schema.primaryDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql.instance;
202
+ const instanceName = (_b = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.cloudSql.instance;
160
203
  if (!instanceName) {
161
204
  throw new error_1.FirebaseError("tried to migrate schema but instance name was not provided in dataconnect.yaml");
162
205
  }
@@ -218,28 +261,38 @@ async function handleIncompatibleSchemaError(args) {
218
261
  }
219
262
  return [];
220
263
  }
221
- async function promptForSchemaMigration(options, instanceName, databaseId, err, validateOnly, schemaValidation) {
264
+ async function promptForSchemaMigration(options, instanceName, databaseId, err, validateOnly, validationMode) {
222
265
  if (!err) {
223
266
  return "none";
224
267
  }
225
- displaySchemaChanges(err, schemaValidation, instanceName, databaseId);
268
+ if (validationMode === "STRICT_AFTER_COMPATIBLE" && (options.nonInteractive || options.force)) {
269
+ return "none";
270
+ }
271
+ displaySchemaChanges(err, validationMode, instanceName, databaseId);
226
272
  if (!options.nonInteractive) {
227
273
  if (validateOnly && options.force) {
228
274
  return "all";
229
275
  }
230
- const choices = err.destructive
231
- ? [
232
- { name: "Execute all changes (including destructive changes)", value: "all" },
233
- { name: "Abort changes", value: "none" },
234
- ]
235
- : [
236
- { name: "Execute changes", value: "all" },
237
- { name: "Abort changes", value: "none" },
238
- ];
276
+ const message = validationMode === "STRICT_AFTER_COMPATIBLE"
277
+ ? `Would you like to execute these optional changes against ${databaseId} in your CloudSQL instance ${instanceName}?`
278
+ : `Would you like to execute these changes against ${databaseId} in your CloudSQL instance ${instanceName}?`;
279
+ let executeChangePrompt = "Execute changes";
280
+ if (validationMode === "STRICT_AFTER_COMPATIBLE") {
281
+ executeChangePrompt = "Execute optional changes";
282
+ }
283
+ if (err.destructive) {
284
+ executeChangePrompt = executeChangePrompt + " (including destructive changes)";
285
+ }
286
+ const choices = [
287
+ { name: executeChangePrompt, value: "all" },
288
+ { name: "Abort changes", value: "none" },
289
+ ];
290
+ const defaultValue = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
239
291
  return await (0, prompt_1.promptOnce)({
240
- message: `Would you like to execute these changes against ${databaseId}?`,
292
+ message: message,
241
293
  type: "list",
242
294
  choices,
295
+ default: defaultValue,
243
296
  });
244
297
  }
245
298
  if (!validateOnly) {
@@ -261,10 +314,7 @@ async function promptForInvalidConnectorError(options, serviceName, invalidConne
261
314
  }
262
315
  displayInvalidConnectors(invalidConnectors);
263
316
  if (validateOnly) {
264
- if (options.force) {
265
- return false;
266
- }
267
- throw new error_1.FirebaseError(`Command aborted. If you'd like to migrate it anyway, you may override with --force.`);
317
+ return false;
268
318
  }
269
319
  if (options.force) {
270
320
  return true;
@@ -297,17 +347,20 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceId, datab
297
347
  source: {
298
348
  files: [],
299
349
  },
300
- primaryDatasource: {
301
- postgresql: {
302
- database: databaseId,
303
- cloudSql: {
304
- instance: instanceId,
350
+ datasources: [
351
+ {
352
+ postgresql: {
353
+ database: databaseId,
354
+ cloudSql: {
355
+ instance: instanceId,
356
+ },
305
357
  },
306
358
  },
307
- },
359
+ ],
308
360
  };
309
361
  }
310
- const postgresql = currentSchema.primaryDatasource.postgresql;
362
+ const postgresDatasource = currentSchema.datasources.find((d) => d.postgresql);
363
+ const postgresql = postgresDatasource === null || postgresDatasource === void 0 ? void 0 : postgresDatasource.postgresql;
311
364
  if ((postgresql === null || postgresql === void 0 ? void 0 : postgresql.cloudSql.instance) !== instanceId) {
312
365
  (0, utils_1.logLabeledWarning)("dataconnect", `Switching connected Cloud SQL instance\nFrom ${postgresql === null || postgresql === void 0 ? void 0 : postgresql.cloudSql.instance}\nTo ${instanceId}`);
313
366
  }
@@ -328,22 +381,22 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceId, datab
328
381
  logger_1.logger.debug(err);
329
382
  }
330
383
  }
331
- function displaySchemaChanges(error, schemaValidation, instanceName, databaseId) {
384
+ function displaySchemaChanges(error, validationMode, instanceName, databaseId) {
332
385
  switch (error.violationType) {
333
386
  case "INCOMPATIBLE_SCHEMA":
334
387
  {
335
388
  let message;
336
- if (schemaValidation === "COMPATIBLE") {
389
+ if (validationMode === "COMPATIBLE") {
337
390
  message =
338
- "Your new application schema is incompatible with the schema of your PostgreSQL database " +
391
+ "Your PostgreSQL database " +
339
392
  databaseId +
340
393
  " in your CloudSQL instance " +
341
394
  instanceName +
342
- ". " +
395
+ " must be migrated in order to be compatible with your application schema. " +
343
396
  "The following SQL statements will migrate your database schema to be compatible with your new Data Connect schema.\n" +
344
397
  error.diffs.map(toString).join("\n");
345
398
  }
346
- else if (schemaValidation === "STRICT_AFTER_COMPATIBLE") {
399
+ else if (validationMode === "STRICT_AFTER_COMPATIBLE") {
347
400
  message =
348
401
  "Your new application schema is compatible with the schema of your PostgreSQL database " +
349
402
  databaseId +
@@ -355,11 +408,11 @@ function displaySchemaChanges(error, schemaValidation, instanceName, databaseId)
355
408
  }
356
409
  else {
357
410
  message =
358
- "Your new application schema does not match the schema of your PostgreSQL database " +
411
+ "Your PostgreSQL database " +
359
412
  databaseId +
360
413
  " in your CloudSQL instance " +
361
414
  instanceName +
362
- ". " +
415
+ " must be migrated in order to match your application schema. " +
363
416
  "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
364
417
  error.diffs.map(toString).join("\n");
365
418
  }
@@ -44,20 +44,23 @@ async function default_1(context, options) {
44
44
  })
45
45
  .map(async (s) => {
46
46
  var _a, _b;
47
- const instanceId = (_a = s.schema.primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instance.split("/").pop();
48
- const databaseId = (_b = s.schema.primaryDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.database;
49
- if (!instanceId || !databaseId) {
50
- return Promise.resolve();
47
+ const postgresDatasource = s.schema.datasources.find((d) => d.postgresql);
48
+ if (postgresDatasource) {
49
+ const instanceId = (_a = postgresDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instance.split("/").pop();
50
+ const databaseId = (_b = postgresDatasource.postgresql) === null || _b === void 0 ? void 0 : _b.database;
51
+ if (!instanceId || !databaseId) {
52
+ return Promise.resolve();
53
+ }
54
+ const enableGoogleMlIntegration = (0, types_1.requiresVector)(s.deploymentMetadata);
55
+ return (0, provisionCloudSql_1.provisionCloudSql)({
56
+ projectId,
57
+ locationId: (0, names_1.parseServiceName)(s.serviceName).location,
58
+ instanceId,
59
+ databaseId,
60
+ enableGoogleMlIntegration,
61
+ waitForCreation: true,
62
+ });
51
63
  }
52
- const enableGoogleMlIntegration = (0, types_1.requiresVector)(s.deploymentMetadata);
53
- return (0, provisionCloudSql_1.provisionCloudSql)({
54
- projectId,
55
- locationId: (0, names_1.parseServiceName)(s.serviceName).location,
56
- instanceId,
57
- databaseId,
58
- enableGoogleMlIntegration,
59
- waitForCreation: true,
60
- });
61
64
  }));
62
65
  return;
63
66
  }