firebase-tools 14.13.0 → 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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvalidConnectors = exports.getIncompatibleSchemaError = void 0;
3
+ exports.getGQLErrors = exports.getInvalidConnectors = exports.getIncompatibleSchemaError = void 0;
4
+ const graphqlError_1 = require("./graphqlError");
4
5
  const INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING = "IncompatibleSqlSchemaError";
5
6
  const PRECONDITION_ERROR_TYPESTRING = "type.googleapis.com/google.rpc.PreconditionFailure";
6
7
  const INCOMPATIBLE_CONNECTOR_TYPE = "INCOMPATIBLE_CONNECTOR";
@@ -31,6 +32,11 @@ function getInvalidConnectors(err) {
31
32
  return invalidConns;
32
33
  }
33
34
  exports.getInvalidConnectors = getInvalidConnectors;
35
+ function getGQLErrors(err) {
36
+ const gqlErrs = errorDetails(err, "GraphqlError");
37
+ return gqlErrs.map(graphqlError_1.prettify).join("\n");
38
+ }
39
+ exports.getGQLErrors = getGQLErrors;
34
40
  function errorDetails(err, ofType) {
35
41
  var _a, _b;
36
42
  const original = ((_b = (_a = err.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.error) || (err === null || err === void 0 ? void 0 : err.original);
@@ -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
  }
@@ -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");
@@ -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) {
@@ -329,6 +329,7 @@ class FunctionsEmulator {
329
329
  projectId: this.args.projectId,
330
330
  projectAlias: this.args.projectAlias,
331
331
  isEmulator: true,
332
+ configDir: emulatableBackend.configDir,
332
333
  };
333
334
  const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
334
335
  const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment);
@@ -879,6 +880,7 @@ class FunctionsEmulator {
879
880
  getUserEnvs(backend) {
880
881
  const projectInfo = {
881
882
  functionsSource: backend.functionsDir,
883
+ configDir: backend.configDir,
882
884
  projectId: this.args.projectId,
883
885
  projectAlias: this.args.projectAlias,
884
886
  isEmulator: true,
@@ -140,7 +140,7 @@ function parseStrict(data) {
140
140
  return envs;
141
141
  }
142
142
  exports.parseStrict = parseStrict;
143
- function findEnvfiles(functionsSource, projectId, projectAlias, isEmulator) {
143
+ function findEnvfiles(configDir, projectId, projectAlias, isEmulator) {
144
144
  const files = [".env"];
145
145
  files.push(`.env.${projectId}`);
146
146
  if (projectAlias) {
@@ -150,26 +150,28 @@ function findEnvfiles(functionsSource, projectId, projectAlias, isEmulator) {
150
150
  files.push(FUNCTIONS_EMULATOR_DOTENV);
151
151
  }
152
152
  return files
153
- .map((f) => path.join(functionsSource, f))
153
+ .map((f) => path.join(configDir, f))
154
154
  .filter(fs.existsSync)
155
155
  .map((p) => path.basename(p));
156
156
  }
157
- function hasUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
158
- return findEnvfiles(functionsSource, projectId, projectAlias, isEmulator).length > 0;
157
+ function hasUserEnvs(opts) {
158
+ const configDir = opts.configDir || opts.functionsSource;
159
+ return findEnvfiles(configDir, opts.projectId, opts.projectAlias, opts.isEmulator).length > 0;
159
160
  }
160
161
  exports.hasUserEnvs = hasUserEnvs;
161
162
  function writeUserEnvs(toWrite, envOpts) {
162
163
  if (Object.keys(toWrite).length === 0) {
163
164
  return;
164
165
  }
165
- const { functionsSource, projectId, projectAlias, isEmulator } = envOpts;
166
- const allEnvFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
166
+ const { projectId, projectAlias, isEmulator } = envOpts;
167
+ const configDir = envOpts.configDir || envOpts.functionsSource;
168
+ const allEnvFiles = findEnvfiles(configDir, projectId, projectAlias, isEmulator);
167
169
  const targetEnvFile = envOpts.isEmulator
168
170
  ? FUNCTIONS_EMULATOR_DOTENV
169
171
  : `.env.${envOpts.projectId}`;
170
172
  const targetEnvFileExists = allEnvFiles.includes(targetEnvFile);
171
173
  if (!targetEnvFileExists) {
172
- fs.writeFileSync(path.join(envOpts.functionsSource, targetEnvFile), "", { flag: "wx" });
174
+ fs.writeFileSync(path.join(configDir, targetEnvFile), "", { flag: "wx" });
173
175
  (0, utils_1.logBullet)(clc.yellow(clc.bold("functions: ")) +
174
176
  `Created new local file ${targetEnvFile} to store param values. We suggest explicitly adding or excluding this file from version control.`);
175
177
  }
@@ -186,7 +188,7 @@ function writeUserEnvs(toWrite, envOpts) {
186
188
  for (const k of Object.keys(toWrite)) {
187
189
  lines += formatUserEnvForWrite(k, toWrite[k]);
188
190
  }
189
- fs.appendFileSync(path.join(functionsSource, targetEnvFile), lines);
191
+ fs.appendFileSync(path.join(configDir, targetEnvFile), lines);
190
192
  }
191
193
  exports.writeUserEnvs = writeUserEnvs;
192
194
  function checkForDuplicateKeys(isEmulator, keys, fullEnv, envsWithoutLocal) {
@@ -210,22 +212,24 @@ function formatUserEnvForWrite(key, value) {
210
212
  }
211
213
  return `${key}=${escapedValue}\n`;
212
214
  }
213
- function loadUserEnvs({ functionsSource, projectId, projectAlias, isEmulator, }) {
215
+ function loadUserEnvs(opts) {
214
216
  var _a;
215
- const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator);
217
+ const configDir = opts.configDir || opts.functionsSource;
218
+ const envFiles = findEnvfiles(configDir, opts.projectId, opts.projectAlias, opts.isEmulator);
216
219
  if (envFiles.length === 0) {
217
220
  return {};
218
221
  }
219
- if (projectAlias) {
220
- if (envFiles.includes(`.env.${projectId}`) && envFiles.includes(`.env.${projectAlias}`)) {
221
- throw new error_1.FirebaseError(`Can't have both dotenv files with projectId (env.${projectId}) ` +
222
- `and projectAlias (.env.${projectAlias}) as extensions.`);
222
+ if (opts.projectAlias) {
223
+ if (envFiles.includes(`.env.${opts.projectId}`) &&
224
+ envFiles.includes(`.env.${opts.projectAlias}`)) {
225
+ throw new error_1.FirebaseError(`Can't have both dotenv files with projectId (env.${opts.projectId}) ` +
226
+ `and projectAlias (.env.${opts.projectAlias}) as extensions.`);
223
227
  }
224
228
  }
225
229
  let envs = {};
226
230
  for (const f of envFiles) {
227
231
  try {
228
- const data = fs.readFileSync(path.join(functionsSource, f), "utf8");
232
+ const data = fs.readFileSync(path.join(configDir, f), "utf8");
229
233
  envs = Object.assign(Object.assign({}, envs), parseStrict(data));
230
234
  }
231
235
  catch (err) {
@@ -72,7 +72,7 @@ async function execute(sqlStatements, opts) {
72
72
  sqlStatements.push("COMMIT;");
73
73
  }
74
74
  for (const s of sqlStatements) {
75
- logFn(`Executing: '${s}'`);
75
+ logFn(`> ${s}`);
76
76
  try {
77
77
  results.push(await conn.query(s));
78
78
  }
@@ -84,6 +84,7 @@ async function execute(sqlStatements, opts) {
84
84
  }
85
85
  }
86
86
  await cleanUpFn();
87
+ logFn(``);
87
88
  return results;
88
89
  }
89
90
  exports.execute = execute;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.brownfieldSqlSetup = exports.setupBrownfieldAsGreenfield = exports.getSchemaMetadata = exports.greenFieldSchemaSetup = exports.setupSQLPermissions = exports.checkSQLRoleIsGranted = exports.fdcSqlRoleMap = exports.SchemaSetupStatus = void 0;
3
+ exports.grantRoleTo = exports.brownfieldSqlSetup = exports.setupBrownfieldAsGreenfield = exports.getSchemaMetadata = exports.greenFieldSchemaSetup = exports.setupSQLPermissions = exports.checkSQLRoleIsGranted = exports.fdcSqlRoleMap = exports.SchemaSetupStatus = void 0;
4
4
  const clc = require("colorette");
5
5
  const permissions_1 = require("./permissions");
6
6
  const cloudsqladmin_1 = require("./cloudsqladmin");
@@ -12,6 +12,7 @@ const connect_1 = require("./connect");
12
12
  const lodash_1 = require("lodash");
13
13
  const connect_2 = require("./connect");
14
14
  const utils = require("../../utils");
15
+ const cloudSqlAdminClient = require("./cloudsqladmin");
15
16
  var SchemaSetupStatus;
16
17
  (function (SchemaSetupStatus) {
17
18
  SchemaSetupStatus["NotSetup"] = "not-setup";
@@ -220,3 +221,11 @@ async function brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, s
220
221
  await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, brownfieldSetupCmds, silent, true);
221
222
  }
222
223
  exports.brownfieldSqlSetup = brownfieldSqlSetup;
224
+ async function grantRoleTo(options, instanceId, databaseId, role, email) {
225
+ const projectId = (0, projectUtils_1.needProjectId)(options);
226
+ const { user, mode } = (0, connect_2.toDatabaseUser)(email);
227
+ await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
228
+ const fdcSqlRole = exports.fdcSqlRoleMap[role](databaseId);
229
+ await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
230
+ }
231
+ exports.grantRoleTo = grantRoleTo;
@@ -154,7 +154,7 @@ async function actuateWithInfo(setup, config, info, options) {
154
154
  path: "./example",
155
155
  files: [
156
156
  {
157
- path: "queries",
157
+ path: "queries.gql",
158
158
  content: operationGql,
159
159
  },
160
160
  ],
@@ -135,7 +135,7 @@ async function generateSdkYaml(targetPlatform, connectorYaml, connectorDir, appD
135
135
  if (targetPlatform === types_1.Platform.IOS) {
136
136
  const swiftSdk = {
137
137
  outputDir: path.relative(connectorDir, path.join(appDir, `dataconnect-generated/swift`)),
138
- package: (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(connectorYaml.connectorId)) + "Connector",
138
+ package: "DataConnectGenerated",
139
139
  };
140
140
  connectorYaml.generate.swiftSdk = swiftSdk;
141
141
  }
@@ -144,7 +144,7 @@ async function generateSdkYaml(targetPlatform, connectorYaml, connectorDir, appD
144
144
  const packageJsonDir = path.relative(connectorDir, appDir);
145
145
  const javascriptSdk = {
146
146
  outputDir: path.relative(connectorDir, path.join(appDir, `dataconnect-generated/js/${pkg}`)),
147
- package: `@firebasegen/${pkg}`,
147
+ package: `@dataconnect/generated`,
148
148
  packageJsonDir,
149
149
  };
150
150
  const packageJson = await (0, fileUtils_1.resolvePackageJson)(appDir);
@@ -161,14 +161,14 @@ async function generateSdkYaml(targetPlatform, connectorYaml, connectorDir, appD
161
161
  const pkg = `${(0, lodash_1.snakeCase)(connectorYaml.connectorId)}_connector`;
162
162
  const dartSdk = {
163
163
  outputDir: path.relative(connectorDir, path.join(appDir, `dataconnect-generated/dart/${pkg}`)),
164
- package: pkg,
164
+ package: "dataconnect_generated",
165
165
  };
166
166
  connectorYaml.generate.dartSdk = dartSdk;
167
167
  }
168
168
  if (targetPlatform === types_1.Platform.ANDROID) {
169
169
  const kotlinSdk = {
170
170
  outputDir: path.relative(connectorDir, path.join(appDir, `dataconnect-generated/kotlin`)),
171
- package: `connectors.${(0, lodash_1.snakeCase)(connectorYaml.connectorId)}`,
171
+ package: `com.google.firebase.dataconnect.generated`,
172
172
  };
173
173
  for (const candidateSubdir of ["app/src/main/java", "app/src/main/kotlin"]) {
174
174
  const candidateDir = path.join(appDir, candidateSubdir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "14.13.0",
3
+ "version": "14.14.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -247,6 +247,9 @@
247
247
  "codebase": {
248
248
  "type": "string"
249
249
  },
250
+ "configDir": {
251
+ "type": "string"
252
+ },
250
253
  "ignore": {
251
254
  "items": {
252
255
  "type": "string"