firebase-tools 13.33.0 → 13.35.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 (38) hide show
  1. package/lib/apphosting/config.js +8 -6
  2. package/lib/apphosting/yaml.js +21 -48
  3. package/lib/commands/dataconnect-sdk-generate.js +4 -1
  4. package/lib/commands/dataconnect-sql-diff.js +1 -1
  5. package/lib/commands/dataconnect-sql-grant.js +2 -2
  6. package/lib/commands/dataconnect-sql-setup.js +40 -0
  7. package/lib/commands/functions-artifacts-setpolicy.js +125 -0
  8. package/lib/commands/index.js +3 -0
  9. package/lib/commands/open.js +3 -0
  10. package/lib/dataconnect/build.js +3 -1
  11. package/lib/dataconnect/fileUtils.js +8 -4
  12. package/lib/dataconnect/schemaMigration.js +38 -12
  13. package/lib/defaultCredentials.js +12 -1
  14. package/lib/deploy/dataconnect/prepare.js +1 -1
  15. package/lib/deploy/functions/containerCleaner.js +17 -2
  16. package/lib/deploy/functions/runtimes/discovery/index.js +3 -1
  17. package/lib/deploy/index.js +22 -20
  18. package/lib/emulator/ExpressBasedEmulator.js +1 -1
  19. package/lib/emulator/apphosting/index.js +1 -0
  20. package/lib/emulator/apphosting/serve.js +48 -7
  21. package/lib/emulator/controller.js +1 -0
  22. package/lib/emulator/dataconnect/pgliteServer.js +7 -2
  23. package/lib/emulator/dataconnectEmulator.js +15 -4
  24. package/lib/emulator/downloadableEmulators.js +9 -9
  25. package/lib/emulator/env.js +17 -1
  26. package/lib/emulator/functionsEmulator.js +2 -20
  27. package/lib/extensions/extensionsHelper.js +8 -4
  28. package/lib/frameworks/angular/utils.js +60 -42
  29. package/lib/functions/artifacts.js +104 -0
  30. package/lib/gcp/artifactregistry.js +27 -2
  31. package/lib/gcp/cloudsql/cloudsqladmin.js +22 -1
  32. package/lib/gcp/cloudsql/connect.js +21 -14
  33. package/lib/gcp/cloudsql/permissions.js +45 -99
  34. package/lib/gcp/cloudsql/permissions_setup.js +215 -0
  35. package/lib/init/features/dataconnect/index.js +1 -1
  36. package/lib/init/features/dataconnect/sdk.js +23 -3
  37. package/package.json +1 -1
  38. package/templates/init/dataconnect/connector.yaml +1 -1
@@ -0,0 +1,215 @@
1
+ "use strict";
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;
4
+ const permissions_1 = require("./permissions");
5
+ const cloudsqladmin_1 = require("./cloudsqladmin");
6
+ const connect_1 = require("./connect");
7
+ const logger_1 = require("../../logger");
8
+ const prompt_1 = require("../../prompt");
9
+ const clc = require("colorette");
10
+ const error_1 = require("../../error");
11
+ const projectUtils_1 = require("../../projectUtils");
12
+ const connect_2 = require("./connect");
13
+ const lodash_1 = require("lodash");
14
+ const connect_3 = require("./connect");
15
+ var SchemaSetupStatus;
16
+ (function (SchemaSetupStatus) {
17
+ SchemaSetupStatus["NotSetup"] = "not-setup";
18
+ SchemaSetupStatus["GreenField"] = "greenfield";
19
+ SchemaSetupStatus["BrownField"] = "brownfield";
20
+ SchemaSetupStatus["NotFound"] = "not-found";
21
+ })(SchemaSetupStatus = exports.SchemaSetupStatus || (exports.SchemaSetupStatus = {}));
22
+ exports.fdcSqlRoleMap = {
23
+ owner: permissions_1.firebaseowner,
24
+ writer: permissions_1.firebasewriter,
25
+ reader: permissions_1.firebasereader,
26
+ };
27
+ async function checkSQLRoleIsGranted(options, instanceId, databaseId, grantedRole, granteeRole) {
28
+ const checkCmd = `
29
+ DO $$
30
+ DECLARE
31
+ role_count INTEGER;
32
+ BEGIN
33
+ -- Count the number of rows matching the criteria
34
+ SELECT COUNT(*)
35
+ INTO role_count
36
+ FROM
37
+ pg_auth_members m
38
+ JOIN
39
+ pg_roles grantee ON grantee.oid = m.member
40
+ JOIN
41
+ pg_roles granted ON granted.oid = m.roleid
42
+ JOIN
43
+ pg_roles grantor ON grantor.oid = m.grantor
44
+ WHERE
45
+ granted.rolname = '${grantedRole}'
46
+ AND grantee.rolname = '${granteeRole}';
47
+
48
+ -- If no rows were found, raise an exception
49
+ IF role_count = 0 THEN
50
+ RAISE EXCEPTION 'Role "%", is not granted to role "%".', '${grantedRole}', '${granteeRole}';
51
+ END IF;
52
+ END $$;
53
+ `;
54
+ try {
55
+ await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [checkCmd], true);
56
+ return true;
57
+ }
58
+ catch (e) {
59
+ if (e instanceof error_1.FirebaseError && e.message.includes("not granted to role")) {
60
+ return false;
61
+ }
62
+ logger_1.logger.error(`Role Check Failed: ${e}`);
63
+ throw e;
64
+ }
65
+ }
66
+ exports.checkSQLRoleIsGranted = checkSQLRoleIsGranted;
67
+ async function setupSQLPermissions(instanceId, databaseId, schemaInfo, options, silent = false) {
68
+ const schema = schemaInfo.name;
69
+ logger_1.logger.info(`Detected schema "${schema}" setup status is ${schemaInfo.setupStatus}. Running setup...`);
70
+ const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
71
+ if (!userIsCSQLAdmin) {
72
+ throw new error_1.FirebaseError(`Missing required IAM permission to setup SQL schemas. SQL schema setup requires 'roles/cloudsql.admin' or an equivalent role.`);
73
+ }
74
+ await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
75
+ let runGreenfieldSetup = false;
76
+ if (schemaInfo.setupStatus === SchemaSetupStatus.GreenField) {
77
+ runGreenfieldSetup = true;
78
+ logger_1.logger.info(`Database ${databaseId} has already been setup as greenfield project. Rerunning setup to repair any missing permissions.`);
79
+ }
80
+ if (schemaInfo.tables.length === 0) {
81
+ runGreenfieldSetup = true;
82
+ logger_1.logger.info(`Found no tables in schema "${schema}", assuming greenfield project.`);
83
+ }
84
+ if (runGreenfieldSetup) {
85
+ const greenfieldSetupCmds = await greenFieldSchemaSetup(instanceId, databaseId, schema, options);
86
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, greenfieldSetupCmds, silent, true);
87
+ logger_1.logger.info(clc.green("Database setup complete."));
88
+ return SchemaSetupStatus.GreenField;
89
+ }
90
+ if (options.nonInteractive || options.force) {
91
+ throw new error_1.FirebaseError(`Schema "${schema}" isn't set up and can only be set up in interactive mode.`);
92
+ }
93
+ const currentTablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))];
94
+ logger_1.logger.info(`We found some existing object owners [${currentTablesOwners.join(", ")}] in your cloudsql "${schema}" schema.`);
95
+ const shouldSetupGreenfield = await (0, prompt_1.confirm)({
96
+ message: clc.yellow("Would you like FDC to handle SQL migrations for you moving forward?\n" +
97
+ `This means we will transfer schema and tables ownership to ${(0, permissions_1.firebaseowner)(databaseId, schema)}\n` +
98
+ "Note: your existing migration tools/roles may lose access."),
99
+ default: false,
100
+ });
101
+ if (shouldSetupGreenfield) {
102
+ await setupBrownfieldAsGreenfield(instanceId, databaseId, schemaInfo, options, silent);
103
+ logger_1.logger.info(clc.green("Database setup complete."));
104
+ logger_1.logger.info(clc.yellow("IMPORTANT: please uncomment 'schemaValidation: \"COMPATIBLE\"' in your dataconnect.yaml file to avoid dropping any existing tables by mistake."));
105
+ return SchemaSetupStatus.GreenField;
106
+ }
107
+ else {
108
+ logger_1.logger.info(clc.yellow("Setting up database in brownfield mode.\n" +
109
+ `Note: SQL migrations can't be done through ${clc.bold("firebase dataconnect:sql:migrate")} in this mode.`));
110
+ await brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, silent);
111
+ logger_1.logger.info(clc.green("Brownfield database setup complete."));
112
+ return SchemaSetupStatus.BrownField;
113
+ }
114
+ }
115
+ exports.setupSQLPermissions = setupSQLPermissions;
116
+ async function greenFieldSchemaSetup(instanceId, databaseId, schema, options) {
117
+ const revokes = [];
118
+ if (await checkSQLRoleIsGranted(options, instanceId, databaseId, "cloudsqlsuperuser", (0, permissions_1.firebaseowner)(databaseId))) {
119
+ logger_1.logger.warn("Detected cloudsqlsuperuser was previously given to firebase owner, revoking to improve database security.");
120
+ revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${(0, permissions_1.firebaseowner)(databaseId)}"`);
121
+ }
122
+ const user = (await (0, connect_2.getIAMUser)(options)).user;
123
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
124
+ const { user: fdcP4SAUser } = (0, connect_3.toDatabaseUser)((0, connect_3.getDataConnectP4SA)(projectNumber));
125
+ const sqlRoleSetupCmds = (0, lodash_1.concat)(revokes, [`CREATE SCHEMA IF NOT EXISTS "${schema}"`], (0, permissions_1.ownerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), (0, permissions_1.writerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), (0, permissions_1.readerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), `GRANT "${(0, permissions_1.firebaseowner)(databaseId, schema)}" TO "${user}"`, `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${fdcP4SAUser}"`, (0, permissions_1.defaultPermissions)(databaseId, schema, (0, permissions_1.firebaseowner)(databaseId, schema)));
126
+ return sqlRoleSetupCmds;
127
+ }
128
+ exports.greenFieldSchemaSetup = greenFieldSchemaSetup;
129
+ async function getSchemaMetadata(instanceId, databaseId, schema, options) {
130
+ const checkSchemaExists = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [
131
+ `SELECT pg_get_userbyid(nspowner)
132
+ FROM pg_namespace
133
+ WHERE nspname = '${schema}';`,
134
+ ], true);
135
+ if (!checkSchemaExists[0].rows[0]) {
136
+ return {
137
+ name: schema,
138
+ owner: null,
139
+ setupStatus: SchemaSetupStatus.NotFound,
140
+ tables: [],
141
+ };
142
+ }
143
+ const schemaOwner = checkSchemaExists[0].rows[0].pg_get_userbyid;
144
+ const cmd = `SELECT tablename, tableowner FROM pg_tables WHERE schemaname='${schema}'`;
145
+ const res = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [cmd], true);
146
+ const tables = res[0].rows.map((row) => {
147
+ return {
148
+ name: row.tablename,
149
+ owner: row.tableowner,
150
+ };
151
+ });
152
+ const checkRoleExists = async (role) => {
153
+ const cmd = [`SELECT to_regrole('"${role}"') IS NOT NULL AS exists;`];
154
+ const result = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, cmd, true);
155
+ return result[0].rows[0].exists;
156
+ };
157
+ let setupStatus;
158
+ if (!(await checkRoleExists((0, permissions_1.firebasewriter)(databaseId, schema)))) {
159
+ setupStatus = SchemaSetupStatus.NotSetup;
160
+ }
161
+ else if (tables.every((table) => table.owner === (0, permissions_1.firebaseowner)(databaseId, schema)) &&
162
+ schemaOwner === (0, permissions_1.firebaseowner)(databaseId, schema)) {
163
+ setupStatus = SchemaSetupStatus.GreenField;
164
+ }
165
+ else {
166
+ setupStatus = SchemaSetupStatus.BrownField;
167
+ }
168
+ return {
169
+ name: schema,
170
+ owner: schemaOwner,
171
+ setupStatus,
172
+ tables: tables,
173
+ };
174
+ }
175
+ exports.getSchemaMetadata = getSchemaMetadata;
176
+ async function setupBrownfieldAsGreenfield(instanceId, databaseId, schemaInfo, options, silent = false) {
177
+ const schema = schemaInfo.name;
178
+ const firebaseOwnerRole = (0, permissions_1.firebaseowner)(databaseId, schema);
179
+ const nonFirebasetablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))].filter((owner) => owner !== firebaseOwnerRole);
180
+ const grantOwnersToSuperuserCmds = nonFirebasetablesOwners.map((owner) => `GRANT "${owner}" TO "${permissions_1.FIREBASE_SUPER_USER}"`);
181
+ const revokeOwnersFromSuperuserCmds = nonFirebasetablesOwners.map((owner) => `REVOKE "${owner}" FROM "${permissions_1.FIREBASE_SUPER_USER}"`);
182
+ const greenfieldSetupCmds = await greenFieldSchemaSetup(instanceId, databaseId, schema, options);
183
+ const grantCmds = nonFirebasetablesOwners.map((owner) => `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${owner}"`);
184
+ const alterTableCmds = schemaInfo.tables.map((table) => `ALTER TABLE "${schema}"."${table.name}" OWNER TO "${firebaseOwnerRole}";`);
185
+ const setupCmds = [
186
+ ...grantOwnersToSuperuserCmds,
187
+ ...greenfieldSetupCmds,
188
+ ...grantCmds,
189
+ ...alterTableCmds,
190
+ ...revokeOwnersFromSuperuserCmds,
191
+ ];
192
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, setupCmds, silent, true);
193
+ }
194
+ exports.setupBrownfieldAsGreenfield = setupBrownfieldAsGreenfield;
195
+ async function brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, silent = false) {
196
+ const schema = schemaInfo.name;
197
+ const uniqueTablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))];
198
+ const grantOwnersToFirebasesuperuser = uniqueTablesOwners.map((owner) => `GRANT "${owner}" TO "${permissions_1.FIREBASE_SUPER_USER}"`);
199
+ const revokeOwnersFromFirebasesuperuser = uniqueTablesOwners.map((owner) => `REVOKE "${owner}" FROM "${permissions_1.FIREBASE_SUPER_USER}"`);
200
+ const iamUser = (await (0, connect_2.getIAMUser)(options)).user;
201
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
202
+ const { user: fdcP4SAUser } = (0, connect_3.toDatabaseUser)((0, connect_3.getDataConnectP4SA)(projectNumber));
203
+ const firebaseDefaultPermissions = uniqueTablesOwners.flatMap((owner) => (0, permissions_1.defaultPermissions)(databaseId, schema, owner));
204
+ const brownfieldSetupCmds = [
205
+ ...grantOwnersToFirebasesuperuser,
206
+ ...(0, permissions_1.writerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema),
207
+ ...(0, permissions_1.readerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema),
208
+ `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${iamUser}"`,
209
+ `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${fdcP4SAUser}"`,
210
+ ...firebaseDefaultPermissions,
211
+ ...revokeOwnersFromFirebasesuperuser,
212
+ ];
213
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, brownfieldSetupCmds, silent, true);
214
+ }
215
+ exports.brownfieldSqlSetup = brownfieldSqlSetup;
@@ -59,7 +59,7 @@ async function doSetup(setup, config) {
59
59
  await sdk.doSetup(setup, config);
60
60
  }
61
61
  else {
62
- (0, utils_1.logBullet)(`If you'd like to add the generated SDK to your app your, run ${clc.bold("firebase init dataconnect:sdk")}`);
62
+ (0, utils_1.logBullet)(`If you'd like to add the generated SDK to your app later, run ${clc.bold("firebase init dataconnect:sdk")}`);
63
63
  }
64
64
  if (setup.projectId && !isBillingEnabled) {
65
65
  (0, utils_1.logBullet)((0, freeTrial_1.upgradeInstructions)(setup.projectId));
@@ -14,6 +14,7 @@ const dataconnectEmulator_1 = require("../../../emulator/dataconnectEmulator");
14
14
  const error_1 = require("../../../error");
15
15
  const lodash_1 = require("lodash");
16
16
  const utils_1 = require("../../../utils");
17
+ const auth_1 = require("../../../auth");
17
18
  exports.FDC_APP_FOLDER = "_FDC_APP_FOLDER";
18
19
  async function doSetup(setup, config) {
19
20
  const sdkInfo = await askQuestions(setup, config);
@@ -78,12 +79,13 @@ async function askQuestions(setup, config) {
78
79
  const newConnectorYaml = await generateSdkYaml(targetPlatform, connectorYaml, connectorInfo.directory, appDir);
79
80
  if (targetPlatform === types_1.Platform.WEB) {
80
81
  const unusedFrameworks = fileUtils_1.SUPPORTED_FRAMEWORKS.filter((framework) => { var _a; return !((_a = newConnectorYaml.generate) === null || _a === void 0 ? void 0 : _a.javascriptSdk[framework]); });
82
+ const hasFrameworkEnabled = unusedFrameworks.length < fileUtils_1.SUPPORTED_FRAMEWORKS.length;
81
83
  if (unusedFrameworks.length > 0) {
82
84
  const additionalFrameworks = await (0, prompt_1.prompt)(setup, [
83
85
  {
84
86
  type: "checkbox",
85
87
  name: "fdcFrameworks",
86
- message: "Which framework would you like to generate SDKs for? " +
88
+ message: `Which ${hasFrameworkEnabled && "additional "}frameworks would you like to generate SDKs for? ` +
87
89
  "Press Space to select features, then Enter to confirm your choices.",
88
90
  choices: unusedFrameworks.map((frameworkStr) => ({
89
91
  value: frameworkStr,
@@ -125,6 +127,7 @@ async function generateSdkYaml(targetPlatform, connectorYaml, connectorDir, appD
125
127
  if (packageJson) {
126
128
  const frameworksUsed = (0, fileUtils_1.getFrameworksFromPackageJson)(packageJson);
127
129
  for (const framework of frameworksUsed) {
130
+ (0, utils_1.logBullet)(`Detected ${framework} app. Enabling ${framework} generated SDKs.`);
128
131
  javascriptSdk[framework] = true;
129
132
  }
130
133
  }
@@ -155,17 +158,34 @@ async function generateSdkYaml(targetPlatform, connectorYaml, connectorDir, appD
155
158
  }
156
159
  exports.generateSdkYaml = generateSdkYaml;
157
160
  async function actuate(sdkInfo) {
158
- var _a;
161
+ var _a, _b;
159
162
  const connectorYamlPath = `${sdkInfo.connectorInfo.directory}/connector.yaml`;
160
163
  fs.writeFileSync(connectorYamlPath, sdkInfo.connectorYamlContents, "utf8");
161
164
  (0, utils_1.logBullet)(`Wrote new config to ${connectorYamlPath}`);
165
+ const account = (0, auth_1.getGlobalDefaultAccount)();
162
166
  await dataconnectEmulator_1.DataConnectEmulator.generate({
163
167
  configDir: sdkInfo.connectorInfo.directory,
164
168
  connectorId: sdkInfo.connectorInfo.connectorYaml.connectorId,
169
+ account,
165
170
  });
166
171
  (0, utils_1.logBullet)(`Generated SDK code for ${sdkInfo.connectorInfo.connectorYaml.connectorId}`);
167
172
  if (((_a = sdkInfo.connectorInfo.connectorYaml.generate) === null || _a === void 0 ? void 0 : _a.swiftSdk) && sdkInfo.displayIOSWarning) {
168
- (0, utils_1.logBullet)(clc.bold("Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/gp/ios-sdk#set-client"));
173
+ (0, utils_1.logBullet)(clc.bold("Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/ios-sdk#set-client"));
174
+ }
175
+ if ((_b = sdkInfo.connectorInfo.connectorYaml.generate) === null || _b === void 0 ? void 0 : _b.javascriptSdk) {
176
+ for (const framework of fileUtils_1.SUPPORTED_FRAMEWORKS) {
177
+ if (sdkInfo.connectorInfo.connectorYaml.generate.javascriptSdk[framework]) {
178
+ logInfoForFramework(framework);
179
+ }
180
+ }
169
181
  }
170
182
  }
171
183
  exports.actuate = actuate;
184
+ function logInfoForFramework(framework) {
185
+ if (framework === "react") {
186
+ (0, utils_1.logBullet)("Visit https://firebase.google.com/docs/data-connect/web-sdk#react for more information on how to set up React Generated SDKs for Firebase Data Connect");
187
+ }
188
+ else if (framework === "angular") {
189
+ (0, utils_1.logBullet)("Run `npm i --save @angular/fire @tanstack-query-firebase/angular @tanstack/angular-query-experimental` to install angular sdk dependencies.\nVisit https://github.com/invertase/tanstack-query-firebase/tree/main/packages/angular for more information on how to set up Angular Generated SDKs for Firebase Data Connect");
190
+ }
191
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.33.0",
3
+ "version": "13.35.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@ connectorId: __connectorId__
8
8
  # packageJsonDir: < Optional. Path to your Javascript app's package.json>
9
9
  # swiftSdk:
10
10
  # outputDir: <Path where you want the generated SDK to be written to, relative to this file>
11
- # package: "firebasegen/default"
11
+ # package: DefaultConnector
12
12
  # kotlinSdk:
13
13
  # outputDir: <Path where you want the generated SDK to be written to, relative to this file>
14
14
  # package: connectors.default