firebase-tools 15.4.0 → 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 +6 -1
- package/lib/appdistribution/client.js +2 -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/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/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/downloadableEmulatorInfo.json +24 -24
- package/lib/ensureApiEnabled.js +2 -2
- 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/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;
|
|
@@ -31,6 +31,7 @@ exports.STANDARD_HEADERS = {
|
|
|
31
31
|
const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
|
|
32
32
|
const GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
|
|
33
33
|
const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
|
|
34
|
+
exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869";
|
|
34
35
|
let accessToken = "";
|
|
35
36
|
let refreshToken = "";
|
|
36
37
|
function setRefreshToken(token = "") {
|
|
@@ -128,6 +129,10 @@ class Client {
|
|
|
128
129
|
return await this.doRequest(internalReqOptions);
|
|
129
130
|
}
|
|
130
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
|
+
}
|
|
131
136
|
if (thrown instanceof error_1.FirebaseError) {
|
|
132
137
|
throw thrown;
|
|
133
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;
|
|
@@ -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
|
+
});
|
|
@@ -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
|
}
|
|
@@ -205,6 +205,7 @@ async function prepare(context, options, payload) {
|
|
|
205
205
|
await (0, checkIam_1.ensureGenkitMonitoringRoles)(projectId, projectNumber, matchingBackend, haveBackend, options.dryRun);
|
|
206
206
|
await ensure.secretAccess(projectId, matchingBackend, haveBackend, options.dryRun);
|
|
207
207
|
updateEndpointTargetedStatus(wantBackends, context.filters || []);
|
|
208
|
+
validate.checkFiltersIntegrity(wantBackends, context.filters);
|
|
208
209
|
(0, applyHash_1.applyBackendHashToBackends)(wantBackends, context);
|
|
209
210
|
}
|
|
210
211
|
function inferDetailsFromExisting(want, have, usedDotenv) {
|
|
@@ -8,6 +8,7 @@ exports.endpointsAreUnique = endpointsAreUnique;
|
|
|
8
8
|
exports.functionsDirectoryExists = functionsDirectoryExists;
|
|
9
9
|
exports.functionIdsAreValid = functionIdsAreValid;
|
|
10
10
|
exports.secretsAreValid = secretsAreValid;
|
|
11
|
+
exports.checkFiltersIntegrity = checkFiltersIntegrity;
|
|
11
12
|
const path = require("path");
|
|
12
13
|
const clc = require("colorette");
|
|
13
14
|
const error_1 = require("../../error");
|
|
@@ -257,3 +258,27 @@ async function validateSecretVersions(projectId, endpoints) {
|
|
|
257
258
|
}
|
|
258
259
|
}
|
|
259
260
|
}
|
|
261
|
+
function checkFiltersIntegrity(wantBackends, filters) {
|
|
262
|
+
if (!filters) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const filter of filters) {
|
|
266
|
+
let matched = false;
|
|
267
|
+
for (const b of Object.values(wantBackends)) {
|
|
268
|
+
if (backend.someEndpoint(b, (e) => (0, functionsDeployHelper_1.endpointMatchesFilter)(e, filter))) {
|
|
269
|
+
matched = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!matched) {
|
|
274
|
+
const parts = [];
|
|
275
|
+
if (filter.codebase) {
|
|
276
|
+
parts.push(filter.codebase);
|
|
277
|
+
}
|
|
278
|
+
if (filter.idChunks) {
|
|
279
|
+
parts.push(filter.idChunks.join("-"));
|
|
280
|
+
}
|
|
281
|
+
throw new error_1.FirebaseError(`No function matches the filter: ${parts.join(":")}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -2243,8 +2243,8 @@ function generateBlockingFunctionJwt(state, event, url, timeoutMs, user, options
|
|
|
2243
2243
|
}
|
|
2244
2244
|
if (user.lastLoginAt || user.createdAt) {
|
|
2245
2245
|
jwt.user_record.metadata = {
|
|
2246
|
-
last_sign_in_time: user.lastLoginAt,
|
|
2247
|
-
creation_time: user.createdAt,
|
|
2246
|
+
last_sign_in_time: user.lastLoginAt ? parseInt(user.lastLoginAt) : undefined,
|
|
2247
|
+
creation_time: user.createdAt ? parseInt(user.createdAt) : undefined,
|
|
2248
2248
|
};
|
|
2249
2249
|
}
|
|
2250
2250
|
if (state.shouldForwardCredentialToBlockingFunction("accessToken")) {
|