firebase-tools 15.3.1 → 15.5.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/apiv2.js +13 -3
  2. package/lib/appdistribution/client.js +2 -1
  3. package/lib/apphosting/rollout.js +1 -1
  4. package/lib/apptesting/parseTestFiles.js +91 -43
  5. package/lib/bin/cli.js +2 -2
  6. package/lib/command.js +3 -0
  7. package/lib/commands/apptesting.js +75 -0
  8. package/lib/commands/dataconnect-compile.js +46 -0
  9. package/lib/commands/dataconnect-execute.js +14 -12
  10. package/lib/commands/firestore-databases-create.js +69 -12
  11. package/lib/commands/firestore-databases-update.js +6 -14
  12. package/lib/commands/index.js +2 -0
  13. package/lib/dataconnect/client.js +2 -1
  14. package/lib/dataconnect/types.js +8 -1
  15. package/lib/deploy/functions/prepare.js +1 -0
  16. package/lib/deploy/functions/validate.js +25 -0
  17. package/lib/emulator/auth/operations.js +2 -2
  18. package/lib/emulator/dataconnect/pgliteServer.js +26 -33
  19. package/lib/emulator/dataconnectEmulator.js +0 -1
  20. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  21. package/lib/ensureApiEnabled.js +2 -2
  22. package/lib/env.js +18 -0
  23. package/lib/firestore/api-types.js +26 -11
  24. package/lib/firestore/api.js +3 -0
  25. package/lib/gcp/iam.js +1 -1
  26. package/lib/gcp/rules.js +5 -2
  27. package/lib/gcp/serviceusage.js +2 -2
  28. package/lib/init/features/firestore/indexes.js +1 -1
  29. package/lib/init/features/{storage.js → storage/index.js} +12 -4
  30. package/lib/init/features/storage/rules.js +20 -0
  31. package/lib/mcp/tools/core/get_security_rules.js +1 -1
  32. package/lib/mcp/tools/crashlytics/reports.js +1 -1
  33. package/lib/mcp/tools/firestore/delete_document.js +1 -1
  34. package/lib/mcp/tools/firestore/query_collection.js +1 -1
  35. package/lib/track.js +1 -16
  36. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  37. package/package.json +3 -2
  38. package/templates/init/apptesting/smoke_test.yaml +1 -1
package/lib/apiv2.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Client = exports.STANDARD_HEADERS = void 0;
3
+ exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.STANDARD_HEADERS = void 0;
4
4
  exports.setRefreshToken = setRefreshToken;
5
5
  exports.setAccessToken = setAccessToken;
6
6
  exports.getAccessToken = getAccessToken;
@@ -13,19 +13,25 @@ const node_fetch_1 = require("node-fetch");
13
13
  const util_1 = require("util");
14
14
  const auth = require("./auth");
15
15
  const error_1 = require("./error");
16
+ const env_1 = require("./env");
16
17
  const logger_1 = require("./logger");
17
18
  const responseToError_1 = require("./responseToError");
18
19
  const FormData = require("form-data");
19
20
  const pkg = require("../package.json");
20
21
  const CLI_VERSION = pkg.version;
22
+ const agent = (0, env_1.detectAIAgent)();
23
+ const agentStr = agent === "unknown" ? "" : ` agent-name/${agent}`;
24
+ const platform = (0, env_1.isFirebaseMcp)() ? "FirebaseMCP" : "FirebaseCLI";
25
+ const clientVersion = `${platform}/${CLI_VERSION}${agentStr}`;
21
26
  exports.STANDARD_HEADERS = {
22
27
  Connection: "keep-alive",
23
- "User-Agent": `FirebaseCLI/${CLI_VERSION}`,
24
- "X-Client-Version": `FirebaseCLI/${CLI_VERSION}`,
28
+ "User-Agent": clientVersion,
29
+ "X-Client-Version": clientVersion,
25
30
  };
26
31
  const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
27
32
  const GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
28
33
  const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
34
+ exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869";
29
35
  let accessToken = "";
30
36
  let refreshToken = "";
31
37
  function setRefreshToken(token = "") {
@@ -123,6 +129,10 @@ class Client {
123
129
  return await this.doRequest(internalReqOptions);
124
130
  }
125
131
  catch (thrown) {
132
+ const originalErrorMessage = thrown.original?.message || thrown.message || "";
133
+ if (originalErrorMessage.includes(exports.CLI_OAUTH_PROJECT_NUMBER)) {
134
+ throw new error_1.FirebaseError("An Internal error has occurred. Please try again in a few minutes. If this error persists, please open an issue at https://github.com/firebase/firebase-tools", { original: thrown });
135
+ }
126
136
  if (thrown instanceof error_1.FirebaseError) {
127
137
  throw thrown;
128
138
  }
@@ -225,7 +225,7 @@ class AppDistributionClient {
225
225
  }
226
226
  utils.logSuccess(`Testers removed from group successfully`);
227
227
  }
228
- async createReleaseTest(releaseName, devices, aiInstruction, loginCredential, testCaseName) {
228
+ async createReleaseTest(releaseName, devices, aiInstruction, loginCredential, testCaseName, displayName) {
229
229
  try {
230
230
  const response = await this.appDistroV1AlphaClient.request({
231
231
  method: "POST",
@@ -235,6 +235,7 @@ class AppDistributionClient {
235
235
  loginCredential,
236
236
  testCase: testCaseName,
237
237
  aiInstructions: aiInstruction,
238
+ displayName: displayName,
238
239
  },
239
240
  });
240
241
  return response.body;
@@ -125,7 +125,7 @@ async function orchestrateRollout(args) {
125
125
  throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
126
126
  "This is an internal error and should be reported");
127
127
  }
128
- throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
128
+ throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}`, { children: [build.error] });
129
129
  }
130
130
  return { rollout, build };
131
131
  }
@@ -7,67 +7,115 @@ const logger_1 = require("../logger");
7
7
  const types_1 = require("./types");
8
8
  const utils_1 = require("../utils");
9
9
  const error_1 = require("../error");
10
- function createFilter(pattern) {
11
- const regex = pattern ? new RegExp(pattern) : undefined;
12
- return (s) => !regex || regex.test(s);
13
- }
14
10
  async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
15
- try {
16
- new URL(targetUri);
17
- }
18
- catch (ex) {
19
- const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
20
- throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
11
+ if (targetUri) {
12
+ try {
13
+ new URL(targetUri);
14
+ }
15
+ catch (ex) {
16
+ const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
17
+ throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
18
+ }
21
19
  }
20
+ const files = await parseTestFilesRecursive({ testDir: dir, targetUri });
21
+ const idToInvocation = files
22
+ .flatMap((file) => file.invocations)
23
+ .reduce((accumulator, invocation) => {
24
+ if (invocation.testCase.id) {
25
+ accumulator[invocation.testCase.id] = invocation;
26
+ }
27
+ return accumulator;
28
+ }, {});
22
29
  const fileFilterFn = createFilter(filePattern);
23
30
  const nameFilterFn = createFilter(namePattern);
24
- async function parseTestFilesRecursive(testDir) {
25
- const items = (0, fsutils_1.listFiles)(testDir);
26
- const results = [];
27
- for (const item of items) {
28
- const path = (0, path_1.join)(testDir, item);
29
- if ((0, fsutils_1.dirExistsSync)(path)) {
30
- results.push(...(await parseTestFilesRecursive(path)));
31
+ const filteredInvocations = files
32
+ .filter((file) => fileFilterFn(file.path))
33
+ .flatMap((file) => file.invocations)
34
+ .filter((invocation) => nameFilterFn(invocation.testCase.displayName));
35
+ return filteredInvocations.map((invocation) => {
36
+ let prerequisiteTestCaseId = invocation.testCase.prerequisiteTestCaseId;
37
+ if (prerequisiteTestCaseId === undefined) {
38
+ return invocation;
39
+ }
40
+ const prerequisiteSteps = [];
41
+ const previousTestCaseIds = new Set();
42
+ while (prerequisiteTestCaseId) {
43
+ if (previousTestCaseIds.has(prerequisiteTestCaseId)) {
44
+ throw new error_1.FirebaseError(`Detected a cycle in prerequisite test cases.`);
31
45
  }
32
- else if (fileFilterFn(path) && (0, fsutils_1.fileExistsSync)(path)) {
33
- try {
34
- const file = await (0, utils_1.readFileFromDirectory)(testDir, item);
35
- const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
36
- const tests = parsedFile.tests;
37
- const defaultConfig = parsedFile.defaultConfig;
38
- if (!tests || !tests.length) {
39
- logger_1.logger.info(`No tests found in ${path}. Ignoring.`);
40
- continue;
41
- }
42
- for (const rawTestDef of parsedFile.tests) {
43
- if (!nameFilterFn(rawTestDef.testName))
44
- continue;
45
- const testDef = toTestDef(rawTestDef, targetUri, defaultConfig);
46
- results.push(testDef);
47
- }
48
- }
49
- catch (ex) {
50
- const errMsg = (0, error_1.getErrMsg)(ex);
51
- const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
52
- logger_1.logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
46
+ previousTestCaseIds.add(prerequisiteTestCaseId);
47
+ const prerequisiteTestCaseInvocation = idToInvocation[prerequisiteTestCaseId];
48
+ if (prerequisiteTestCaseInvocation === undefined) {
49
+ throw new error_1.FirebaseError(`Invalid prerequisiteTestCaseId. There is no test case with id ${prerequisiteTestCaseId}`);
50
+ }
51
+ prerequisiteSteps.unshift(...prerequisiteTestCaseInvocation.testCase.steps);
52
+ prerequisiteTestCaseId = prerequisiteTestCaseInvocation.testCase.prerequisiteTestCaseId;
53
+ }
54
+ return {
55
+ ...invocation,
56
+ testCase: {
57
+ ...invocation.testCase,
58
+ steps: prerequisiteSteps.concat(invocation.testCase.steps),
59
+ },
60
+ };
61
+ });
62
+ }
63
+ function createFilter(pattern) {
64
+ const regex = pattern ? new RegExp(pattern) : undefined;
65
+ return (s) => !regex || regex.test(s);
66
+ }
67
+ async function parseTestFilesRecursive(params) {
68
+ const testDir = params.testDir;
69
+ const targetUri = params.targetUri;
70
+ const items = (0, fsutils_1.listFiles)(testDir);
71
+ const results = [];
72
+ for (const item of items) {
73
+ const path = (0, path_1.join)(testDir, item);
74
+ if ((0, fsutils_1.dirExistsSync)(path)) {
75
+ results.push(...(await parseTestFilesRecursive({ testDir: path, targetUri })));
76
+ }
77
+ else if ((0, fsutils_1.fileExistsSync)(path)) {
78
+ try {
79
+ const file = await (0, utils_1.readFileFromDirectory)(testDir, item);
80
+ logger_1.logger.debug(`Read the file ${file.source}.`);
81
+ const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
82
+ logger_1.logger.debug(`Parsed the file.`);
83
+ const tests = parsedFile.tests;
84
+ logger_1.logger.debug(`There are ${tests.length} tests.`);
85
+ const defaultConfig = parsedFile.defaultConfig;
86
+ if (!tests || !tests.length) {
87
+ logger_1.logger.debug(`No tests found in ${path}. Ignoring.`);
53
88
  continue;
54
89
  }
90
+ const invocations = [];
91
+ for (const rawTestDef of tests) {
92
+ const invocation = toTestCaseInvocation(rawTestDef, targetUri, defaultConfig);
93
+ invocations.push(invocation);
94
+ }
95
+ results.push({ path, invocations: invocations });
96
+ }
97
+ catch (ex) {
98
+ const errMsg = (0, error_1.getErrMsg)(ex);
99
+ const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
100
+ logger_1.logger.debug(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
101
+ continue;
55
102
  }
56
103
  }
57
- return results;
58
104
  }
59
- return parseTestFilesRecursive(dir);
105
+ return results;
60
106
  }
61
- function toTestDef(testDef, targetUri, defaultConfig) {
107
+ function toTestCaseInvocation(testDef, targetUri, defaultConfig) {
62
108
  const steps = testDef.steps ?? [];
63
109
  const route = testDef.testConfig?.route ?? defaultConfig?.route ?? "";
64
110
  const browsers = testDef.testConfig?.browsers ??
65
111
  defaultConfig?.browsers ?? [types_1.Browser.CHROME];
66
112
  return {
67
113
  testCase: {
114
+ id: testDef.id,
115
+ prerequisiteTestCaseId: testDef.prerequisiteTestCaseId,
68
116
  startUri: targetUri + route,
69
- displayName: testDef.testName,
70
- instructions: { steps },
117
+ displayName: testDef.displayName,
118
+ steps: steps,
71
119
  },
72
120
  testExecution: browsers.map((browser) => ({ config: { browser } })),
73
121
  };
package/lib/bin/cli.js CHANGED
@@ -10,10 +10,11 @@ const fs = require("node:fs");
10
10
  const configstore_1 = require("../configstore");
11
11
  const errorOut_1 = require("../errorOut");
12
12
  const logger_1 = require("../logger");
13
+ const experiments_1 = require("../experiments");
14
+ (0, experiments_1.enableExperimentsFromCliEnvVariable)();
13
15
  const client = require("..");
14
16
  const fsutils = require("../fsutils");
15
17
  const utils = require("../utils");
16
- const experiments_1 = require("../experiments");
17
18
  const fetchMOTD_1 = require("../fetchMOTD");
18
19
  const command_1 = require("../command");
19
20
  function cli(pkg) {
@@ -36,7 +37,6 @@ function cli(pkg) {
36
37
  }
37
38
  logger_1.logger.debug("-".repeat(70));
38
39
  logger_1.logger.debug();
39
- (0, experiments_1.enableExperimentsFromCliEnvVariable)();
40
40
  (0, fetchMOTD_1.fetchMOTD)();
41
41
  process.on("exit", (code) => {
42
42
  code = typeof process.exitCode === "number" ? process.exitCode : code;
package/lib/command.js CHANGED
@@ -90,6 +90,9 @@ class Command {
90
90
  const start = process.uptime();
91
91
  const options = (0, lodash_1.last)(args);
92
92
  if (args.length - 1 > cmd._args.length) {
93
+ if (!(0, utils_1.getInheritedOption)(options, "json") && !options.isMCP) {
94
+ (0, logger_1.useConsoleLoggers)();
95
+ }
93
96
  client.errorOut(new error_1.FirebaseError(`Too many arguments. Run ${clc.bold("firebase help " + this.name)} for usage instructions`, { exit: 1 }));
94
97
  return;
95
98
  }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const requireAuth_1 = require("../requireAuth");
5
+ const command_1 = require("../command");
6
+ const logger_1 = require("../logger");
7
+ const clc = require("colorette");
8
+ const parseTestFiles_1 = require("../apptesting/parseTestFiles");
9
+ const ora = require("ora");
10
+ const error_1 = require("../error");
11
+ const marked_1 = require("marked");
12
+ const client_1 = require("../appdistribution/client");
13
+ const distribution_1 = require("../appdistribution/distribution");
14
+ const options_parser_util_1 = require("../appdistribution/options-parser-util");
15
+ const defaultDevices = [
16
+ {
17
+ model: "MediumPhone.arm",
18
+ version: "36",
19
+ locale: "en_US",
20
+ orientation: "portrait",
21
+ },
22
+ ];
23
+ exports.command = new command_1.Command("apptesting:execute <release-binary-file>")
24
+ .description("Run mobile automated tests written in natural language driven by AI")
25
+ .option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
26
+ .option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
27
+ .option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
28
+ .option("--test-dir <test_dir>", "Directory where tests can be found.")
29
+ .option("--test-devices <string>", "semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
30
+ .option("--test-devices-file <string>", "path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
31
+ .before(requireAuth_1.requireAuth)
32
+ .action(async (target, options) => {
33
+ const appName = (0, options_parser_util_1.getAppName)(options);
34
+ const testDir = options.testDir || "tests";
35
+ const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, undefined, options.testFilePattern, options.testNamePattern);
36
+ const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
37
+ if (!tests.length) {
38
+ throw new error_1.FirebaseError("No tests found");
39
+ }
40
+ const invokeSpinner = ora("Requesting test execution");
41
+ let testInvocations;
42
+ let releaseId;
43
+ try {
44
+ const client = new client_1.AppDistributionClient();
45
+ releaseId = await (0, distribution_1.upload)(client, appName, new distribution_1.Distribution(target));
46
+ invokeSpinner.start();
47
+ testInvocations = await invokeTests(client, releaseId, tests, !testDevices.length ? defaultDevices : testDevices);
48
+ invokeSpinner.text = "Test execution requested";
49
+ invokeSpinner.succeed();
50
+ }
51
+ catch (ex) {
52
+ invokeSpinner.fail("Failed to request test execution");
53
+ throw ex;
54
+ }
55
+ logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(testInvocations.length)}`));
56
+ logger_1.logger.info(await (0, marked_1.marked)(`View progress and results in the Firebase Console`));
57
+ });
58
+ function pluralizeTests(numTests) {
59
+ return `${numTests} test${numTests === 1 ? "" : "s"}`;
60
+ }
61
+ async function invokeTests(client, releaseName, testDefs, devices) {
62
+ try {
63
+ const testInvocations = [];
64
+ for (const testDef of testDefs) {
65
+ const aiInstruction = {
66
+ steps: testDef.testCase.steps,
67
+ };
68
+ testInvocations.push(await client.createReleaseTest(releaseName, devices, aiInstruction, undefined, undefined, testDef.testCase.displayName));
69
+ }
70
+ return testInvocations;
71
+ }
72
+ catch (err) {
73
+ throw new error_1.FirebaseError("Test invocation failed", { original: (0, error_1.getError)(err) });
74
+ }
75
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const clc = require("colorette");
5
+ const command_1 = require("../command");
6
+ const dataconnectEmulator_1 = require("../emulator/dataconnectEmulator");
7
+ const projectUtils_1 = require("../projectUtils");
8
+ const load_1 = require("../dataconnect/load");
9
+ const auth_1 = require("../auth");
10
+ const utils_1 = require("../utils");
11
+ const hub_1 = require("../emulator/hub");
12
+ const build_1 = require("../dataconnect/build");
13
+ const error_1 = require("../error");
14
+ exports.command = new command_1.Command("dataconnect:compile")
15
+ .description("compile your Data Connect schema and connector config and GQL files.")
16
+ .option("--service <serviceId>", "the serviceId of the Data Connect service. If not provided, compiles all services.")
17
+ .option("--location <location>", "the location of the Data Connect service. Only needed if service ID is used in multiple locations.")
18
+ .action(async (options) => {
19
+ const projectId = (0, projectUtils_1.getProjectId)(options);
20
+ const config = options.config;
21
+ if (!config || !config.has("dataconnect")) {
22
+ throw new error_1.FirebaseError(`No Data Connect project directory found. Please run ${clc.bold("firebase init dataconnect")} to set it up first.`);
23
+ }
24
+ const serviceInfos = await (0, load_1.pickServices)(projectId || hub_1.EmulatorHub.MISSING_PROJECT_PLACEHOLDER, config, options.service, options.location);
25
+ if (!serviceInfos.length) {
26
+ throw new error_1.FirebaseError("No Data Connect services found to compile.");
27
+ }
28
+ for (const serviceInfo of serviceInfos) {
29
+ const configDir = serviceInfo.sourceDirectory;
30
+ const account = (0, auth_1.getProjectDefaultAccount)(options.projectRoot);
31
+ const buildArgs = {
32
+ configDir,
33
+ projectId,
34
+ account,
35
+ };
36
+ const buildResult = await dataconnectEmulator_1.DataConnectEmulator.build(buildArgs);
37
+ if (buildResult?.errors?.length) {
38
+ await (0, build_1.handleBuildErrors)(buildResult.errors, options.nonInteractive, options.force, false);
39
+ }
40
+ await dataconnectEmulator_1.DataConnectEmulator.generate({
41
+ configDir,
42
+ account,
43
+ });
44
+ (0, utils_1.logLabeledSuccess)("dataconnect", `Successfully compiled Data Connect service: ${clc.bold(serviceInfo.dataConnectYaml.serviceId)}`);
45
+ }
46
+ });
@@ -106,13 +106,15 @@ exports.command = new command_1.Command("dataconnect:execute [file] [operationNa
106
106
  operationName,
107
107
  variables: parseJsonObject(unparsedVars, "--variables"),
108
108
  });
109
- let err = (0, responseToError_1.responseToError)(response, response.body);
110
- if ((0, types_1.isGraphQLResponseError)(response.body)) {
111
- const { status, message } = response.body.error;
109
+ const body = response.body;
110
+ let err = (0, responseToError_1.responseToError)(response, body);
111
+ if ((0, types_1.isGraphQLResponseError)(body)) {
112
+ const status = body.status || body.error?.status;
113
+ const message = body.message || body.error?.message || "unknown error";
112
114
  if (!err) {
113
115
  err = new error_1.FirebaseError(message, {
114
116
  context: {
115
- body: response.body,
117
+ body: body,
116
118
  response: response,
117
119
  },
118
120
  status: response.status,
@@ -125,35 +127,35 @@ exports.command = new command_1.Command("dataconnect:execute [file] [operationNa
125
127
  if (err) {
126
128
  throw err;
127
129
  }
128
- if (!(0, types_1.isGraphQLResponse)(response.body)) {
130
+ if (!(0, types_1.isGraphQLResponse)(body)) {
129
131
  throw new error_1.FirebaseError("Got invalid response body with neither .data or .errors", {
130
132
  context: {
131
- body: response.body,
133
+ body: body,
132
134
  response: response,
133
135
  },
134
136
  status: response.status,
135
137
  });
136
138
  }
137
- logger_1.logger.info(JSON.stringify(response.body, null, 2));
138
- if (!response.body.data) {
139
+ logger_1.logger.info(JSON.stringify(body, null, 2));
140
+ if (!body.data) {
139
141
  throw new error_1.FirebaseError("GraphQL request error(s). See response body (above) for details.", {
140
142
  context: {
141
- body: response.body,
143
+ body: body,
142
144
  response: response,
143
145
  },
144
146
  status: response.status,
145
147
  });
146
148
  }
147
- if (response.body.errors && response.body.errors.length > 0) {
149
+ if (body.errors && body.errors.length > 0) {
148
150
  throw new error_1.FirebaseError("Execution completed with error(s). See response body (above) for details.", {
149
151
  context: {
150
- body: response.body,
152
+ body: body,
151
153
  response: response,
152
154
  },
153
155
  status: response.status,
154
156
  });
155
157
  }
156
- return response.body;
158
+ return body;
157
159
  async function readQueryFromDir(dir) {
158
160
  const opDisplay = operationName ? clc.bold(operationName) : "operation";
159
161
  process.stderr.write(`${clc.cyan(`Executing ${opDisplay} in ${clc.bold(dir)}`)}${node_os_1.EOL}`);
@@ -17,6 +17,9 @@ exports.command = new command_1.Command("firestore:databases:create <database>")
17
17
  .option("--edition <edition>", "the edition of the database to create, for example 'standard' or 'enterprise'. If not provided, 'standard' is used as a default.")
18
18
  .option("--delete-protection <deleteProtectionState>", "whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'")
19
19
  .option("--point-in-time-recovery <enablement>", "whether to enable the PITR feature on this database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'")
20
+ .option("--realtime-updates <enablement>", "Whether realtime updates are enabled for this database, for example 'ENABLED' or 'DISABLED'. Can only be specified for 'enterprise' edition databases. Defaults to 'ENABLED' when firestore-data-access is enabled, otherwise the server default is used.")
21
+ .option("--firestore-data-access <enablement>", "Whether the Firestore API can be used for this database, for example 'ENABLED' or 'DISABLED'. Can only be specified for 'enterprise' edition databases. Default is 'ENABLED'.")
22
+ .option("--mongodb-compatible-data-access <enablement>", "Whether the MongoDB compatible API can be used for this database, for example 'ENABLED' or 'DISABLED'. Can only be specified for 'enterprise' edition databases. Default is 'DISABLED'.")
20
23
  .option("-k, --kms-key-name <kmsKeyName>", "the resource ID of a Cloud KMS key. If set, the database created will be a " +
21
24
  "Customer-managed Encryption Key (CMEK) database encrypted with this key. " +
22
25
  "This feature is allowlist only in initial launch")
@@ -39,22 +42,51 @@ exports.command = new command_1.Command("firestore:databases:create <database>")
39
42
  }
40
43
  databaseEdition = edition;
41
44
  }
42
- if (options.deleteProtection &&
43
- options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
44
- options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED) {
45
- throw new error_1.FirebaseError(`Invalid value for flag --delete-protection. ${helpCommandText}`);
46
- }
47
- const deleteProtectionState = options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED
45
+ types.validateEnablementOption(options.deleteProtection, "delete-protection", helpCommandText);
46
+ const deleteProtectionState = options.deleteProtection === types.EnablementOption.ENABLED
48
47
  ? types.DatabaseDeleteProtectionState.ENABLED
49
48
  : types.DatabaseDeleteProtectionState.DISABLED;
50
- if (options.pointInTimeRecovery &&
51
- options.pointInTimeRecovery !== types.PointInTimeRecoveryEnablementOption.ENABLED &&
52
- options.pointInTimeRecovery !== types.PointInTimeRecoveryEnablementOption.DISABLED) {
53
- throw new error_1.FirebaseError(`Invalid value for flag --point-in-time-recovery. ${helpCommandText}`);
54
- }
55
- const pointInTimeRecoveryEnablement = options.pointInTimeRecovery === types.PointInTimeRecoveryEnablementOption.ENABLED
49
+ types.validateEnablementOption(options.pointInTimeRecovery, "point-in-time-recovery", helpCommandText);
50
+ const pointInTimeRecoveryEnablement = options.pointInTimeRecovery === types.EnablementOption.ENABLED
56
51
  ? types.PointInTimeRecoveryEnablement.ENABLED
57
52
  : types.PointInTimeRecoveryEnablement.DISABLED;
53
+ types.validateEnablementOption(options.firestoreDataAccess, "firestore-data-access", helpCommandText);
54
+ let userFirestoreDataAccess;
55
+ if (options.firestoreDataAccess === types.EnablementOption.ENABLED) {
56
+ userFirestoreDataAccess = types.DataAccessMode.ENABLED;
57
+ }
58
+ else if (options.firestoreDataAccess === types.EnablementOption.DISABLED) {
59
+ userFirestoreDataAccess = types.DataAccessMode.DISABLED;
60
+ }
61
+ types.validateEnablementOption(options.mongodbCompatibleDataAccess, "mongodb-compatible-data-access", helpCommandText);
62
+ let userMongodbDataAccess;
63
+ if (options.mongodbCompatibleDataAccess === types.EnablementOption.ENABLED) {
64
+ userMongodbDataAccess = types.DataAccessMode.ENABLED;
65
+ }
66
+ else if (options.mongodbCompatibleDataAccess === types.EnablementOption.DISABLED) {
67
+ userMongodbDataAccess = types.DataAccessMode.DISABLED;
68
+ }
69
+ let firestoreDataAccessMode = userFirestoreDataAccess;
70
+ if (firestoreDataAccessMode == null) {
71
+ firestoreDataAccessMode = getDefaultFirestoreDataAccessMode(databaseEdition, userMongodbDataAccess);
72
+ }
73
+ let mongodbCompatibleDataAccessMode = userMongodbDataAccess;
74
+ if (mongodbCompatibleDataAccessMode == null) {
75
+ mongodbCompatibleDataAccessMode = getDefaultMongodbDataAccessMode(databaseEdition, userFirestoreDataAccess);
76
+ }
77
+ types.validateEnablementOption(options.realtimeUpdates, "realtime-updates", helpCommandText);
78
+ let realtimeUpdatesMode;
79
+ if (options.realtimeUpdates === types.EnablementOption.ENABLED) {
80
+ realtimeUpdatesMode = types.RealtimeUpdatesMode.ENABLED;
81
+ }
82
+ else if (options.realtimeUpdates === types.EnablementOption.DISABLED) {
83
+ realtimeUpdatesMode = types.RealtimeUpdatesMode.DISABLED;
84
+ }
85
+ if (realtimeUpdatesMode == null &&
86
+ databaseEdition === types.DatabaseEdition.ENTERPRISE &&
87
+ firestoreDataAccessMode === types.DataAccessMode.ENABLED) {
88
+ realtimeUpdatesMode = types.RealtimeUpdatesMode.ENABLED;
89
+ }
58
90
  let cmekConfig;
59
91
  if (options.kmsKeyName) {
60
92
  cmekConfig = {
@@ -69,6 +101,9 @@ exports.command = new command_1.Command("firestore:databases:create <database>")
69
101
  databaseEdition,
70
102
  deleteProtectionState,
71
103
  pointInTimeRecoveryEnablement,
104
+ realtimeUpdatesMode,
105
+ firestoreDataAccessMode,
106
+ mongodbCompatibleDataAccessMode,
72
107
  cmekConfig,
73
108
  };
74
109
  const databaseResp = await api.createDatabase(createDatabaseReq);
@@ -79,3 +114,25 @@ exports.command = new command_1.Command("firestore:databases:create <database>")
79
114
  logger_1.logger.info(`Your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, database)}`);
80
115
  return databaseResp;
81
116
  });
117
+ function getDefaultFirestoreDataAccessMode(databaseEdition, userMongodbDataAccess) {
118
+ if (databaseEdition !== types.DatabaseEdition.ENTERPRISE) {
119
+ return types.DataAccessMode.UNSPECIFIED;
120
+ }
121
+ switch (userMongodbDataAccess) {
122
+ case types.DataAccessMode.ENABLED:
123
+ return types.DataAccessMode.DISABLED;
124
+ default:
125
+ return types.DataAccessMode.ENABLED;
126
+ }
127
+ }
128
+ function getDefaultMongodbDataAccessMode(databaseEdition, userFirestoreDataAccess) {
129
+ if (databaseEdition !== types.DatabaseEdition.ENTERPRISE) {
130
+ return types.DataAccessMode.UNSPECIFIED;
131
+ }
132
+ switch (userFirestoreDataAccess) {
133
+ case types.DataAccessMode.ENABLED:
134
+ return types.DataAccessMode.DISABLED;
135
+ default:
136
+ return types.DataAccessMode.DISABLED;
137
+ }
138
+ }
@@ -25,28 +25,20 @@ exports.command = new command_1.Command("firestore:databases:update <database>")
25
25
  if (!options.deleteProtection && !options.pointInTimeRecovery) {
26
26
  throw new error_1.FirebaseError(`Missing properties to update. ${helpCommandText}`);
27
27
  }
28
- if (options.deleteProtection &&
29
- options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
30
- options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED) {
31
- throw new error_1.FirebaseError(`Invalid value for flag --delete-protection. ${helpCommandText}`);
32
- }
28
+ types.validateEnablementOption(options.deleteProtection, "delete-protection", helpCommandText);
33
29
  let deleteProtectionState;
34
- if (options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED) {
30
+ if (options.deleteProtection === types.EnablementOption.ENABLED) {
35
31
  deleteProtectionState = types.DatabaseDeleteProtectionState.ENABLED;
36
32
  }
37
- else if (options.deleteProtection === types.DatabaseDeleteProtectionStateOption.DISABLED) {
33
+ else if (options.deleteProtection === types.EnablementOption.DISABLED) {
38
34
  deleteProtectionState = types.DatabaseDeleteProtectionState.DISABLED;
39
35
  }
40
- if (options.pointInTimeRecovery &&
41
- options.pointInTimeRecovery !== types.PointInTimeRecoveryEnablementOption.ENABLED &&
42
- options.pointInTimeRecovery !== types.PointInTimeRecoveryEnablementOption.DISABLED) {
43
- throw new error_1.FirebaseError(`Invalid value for flag --point-in-time-recovery. ${helpCommandText}`);
44
- }
36
+ types.validateEnablementOption(options.pointInTimeRecovery, "point-in-time-recovery", helpCommandText);
45
37
  let pointInTimeRecoveryEnablement;
46
- if (options.pointInTimeRecovery === types.PointInTimeRecoveryEnablementOption.ENABLED) {
38
+ if (options.pointInTimeRecovery === types.EnablementOption.ENABLED) {
47
39
  pointInTimeRecoveryEnablement = types.PointInTimeRecoveryEnablement.ENABLED;
48
40
  }
49
- else if (options.pointInTimeRecovery === types.PointInTimeRecoveryEnablementOption.DISABLED) {
41
+ else if (options.pointInTimeRecovery === types.EnablementOption.DISABLED) {
50
42
  pointInTimeRecoveryEnablement = types.PointInTimeRecoveryEnablement.DISABLED;
51
43
  }
52
44
  const databaseResp = await api.updateDatabase(options.project, database, deleteProtectionState, pointInTimeRecoveryEnablement);
@@ -244,6 +244,7 @@ function load(client) {
244
244
  client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
245
245
  client.dataconnect.sql.grant = loadCommand("dataconnect-sql-grant");
246
246
  client.dataconnect.sql.shell = loadCommand("dataconnect-sql-shell");
247
+ client.dataconnect.compile = loadCommand("dataconnect-compile");
247
248
  client.dataconnect.sdk = {};
248
249
  client.dataconnect.sdk.generate = loadCommand("dataconnect-sdk-generate");
249
250
  client.target = loadCommand("target");
@@ -253,6 +254,7 @@ function load(client) {
253
254
  client.use = loadCommand("use");
254
255
  if (experiments.isEnabled("apptesting")) {
255
256
  client.apptesting = {};
257
+ client.apptesting.execute = loadCommand("apptesting");
256
258
  client.apptesting.wata = loadCommand("apptesting-wata");
257
259
  }
258
260
  const t1 = process.hrtime.bigint();
@@ -114,7 +114,7 @@ async function upsertSchema(schema, validateOnly = false, async = false) {
114
114
  apiOrigin: (0, api_1.dataconnectOrigin)(),
115
115
  apiVersion: DATACONNECT_API_VERSION,
116
116
  operationResourceName: op.body.name,
117
- masterTimeout: 10000,
117
+ masterTimeout: 60000,
118
118
  });
119
119
  }
120
120
  async function deleteSchema(serviceName) {
@@ -163,6 +163,7 @@ async function upsertConnector(connector) {
163
163
  apiOrigin: (0, api_1.dataconnectOrigin)(),
164
164
  apiVersion: DATACONNECT_API_VERSION,
165
165
  operationResourceName: op.body.name,
166
+ masterTimeout: 60000,
166
167
  });
167
168
  return pollRes;
168
169
  }