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.
- package/lib/apiv2.js +13 -3
- package/lib/appdistribution/client.js +2 -1
- package/lib/apphosting/rollout.js +1 -1
- package/lib/apptesting/parseTestFiles.js +91 -43
- package/lib/bin/cli.js +2 -2
- package/lib/command.js +3 -0
- package/lib/commands/apptesting.js +75 -0
- package/lib/commands/dataconnect-compile.js +46 -0
- package/lib/commands/dataconnect-execute.js +14 -12
- package/lib/commands/firestore-databases-create.js +69 -12
- package/lib/commands/firestore-databases-update.js +6 -14
- package/lib/commands/index.js +2 -0
- package/lib/dataconnect/client.js +2 -1
- package/lib/dataconnect/types.js +8 -1
- package/lib/deploy/functions/prepare.js +1 -0
- package/lib/deploy/functions/validate.js +25 -0
- package/lib/emulator/auth/operations.js +2 -2
- package/lib/emulator/dataconnect/pgliteServer.js +26 -33
- package/lib/emulator/dataconnectEmulator.js +0 -1
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/ensureApiEnabled.js +2 -2
- package/lib/env.js +18 -0
- package/lib/firestore/api-types.js +26 -11
- package/lib/firestore/api.js +3 -0
- package/lib/gcp/iam.js +1 -1
- package/lib/gcp/rules.js +5 -2
- package/lib/gcp/serviceusage.js +2 -2
- package/lib/init/features/firestore/indexes.js +1 -1
- package/lib/init/features/{storage.js → storage/index.js} +12 -4
- package/lib/init/features/storage/rules.js +20 -0
- package/lib/mcp/tools/core/get_security_rules.js +1 -1
- package/lib/mcp/tools/crashlytics/reports.js +1 -1
- package/lib/mcp/tools/firestore/delete_document.js +1 -1
- package/lib/mcp/tools/firestore/query_collection.js +1 -1
- package/lib/track.js +1 -16
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +3 -2
- 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":
|
|
24
|
-
"X-Client-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}
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
105
|
+
return results;
|
|
60
106
|
}
|
|
61
|
-
function
|
|
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.
|
|
70
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
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)(
|
|
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:
|
|
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(
|
|
138
|
-
if (!
|
|
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:
|
|
143
|
+
body: body,
|
|
142
144
|
response: response,
|
|
143
145
|
},
|
|
144
146
|
status: response.status,
|
|
145
147
|
});
|
|
146
148
|
}
|
|
147
|
-
if (
|
|
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:
|
|
152
|
+
body: body,
|
|
151
153
|
response: response,
|
|
152
154
|
},
|
|
153
155
|
status: response.status,
|
|
154
156
|
});
|
|
155
157
|
}
|
|
156
|
-
return
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
30
|
+
if (options.deleteProtection === types.EnablementOption.ENABLED) {
|
|
35
31
|
deleteProtectionState = types.DatabaseDeleteProtectionState.ENABLED;
|
|
36
32
|
}
|
|
37
|
-
else if (options.deleteProtection === types.
|
|
33
|
+
else if (options.deleteProtection === types.EnablementOption.DISABLED) {
|
|
38
34
|
deleteProtectionState = types.DatabaseDeleteProtectionState.DISABLED;
|
|
39
35
|
}
|
|
40
|
-
|
|
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.
|
|
38
|
+
if (options.pointInTimeRecovery === types.EnablementOption.ENABLED) {
|
|
47
39
|
pointInTimeRecoveryEnablement = types.PointInTimeRecoveryEnablement.ENABLED;
|
|
48
40
|
}
|
|
49
|
-
else if (options.pointInTimeRecovery === types.
|
|
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);
|
package/lib/commands/index.js
CHANGED
|
@@ -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:
|
|
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
|
}
|