firebase-tools 15.9.1 → 15.10.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/appdistribution/distribution.js +1 -1
- package/lib/appdistribution/options-parser-util.js +7 -0
- package/lib/apphosting/config.js +24 -1
- package/lib/commands/appdistribution-distribute.js +5 -5
- package/lib/commands/apptesting.js +9 -10
- package/lib/commands/dataconnect-sql-setup.js +3 -4
- package/lib/commands/dataconnect-sql-shell.js +2 -1
- package/lib/commands/index.js +4 -6
- package/lib/commands/studio-export.js +24 -6
- package/lib/dataconnect/load.js +24 -1
- package/lib/dataconnect/schemaMigration.js +25 -17
- package/lib/dataconnect/types.js +1 -0
- package/lib/deploy/functions/build.js +24 -9
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +8 -2
- package/lib/emulator/apphosting/config.js +1 -21
- package/lib/emulator/controller.js +2 -1
- package/lib/emulator/hub.js +2 -0
- package/lib/emulator/hubExport.js +7 -6
- package/lib/experiments.js +5 -5
- package/lib/firebase_studio/migrate.js +162 -66
- package/lib/gcp/cloudfunctionsv2.js +23 -2
- package/lib/gcp/cloudsql/permissionsSetup.js +4 -4
- package/lib/init/features/hosting/index.js +71 -101
- package/lib/mcp/tools/apptesting/tests.js +2 -2
- package/lib/track.js +1 -1
- package/package.json +1 -1
- package/schema/dataconnect-yaml.json +4 -0
- package/templates/firebase-studio-export/system_instructions_template.md +14 -3
- package/templates/firebase-studio-export/workflows/startup_workflow.md +3 -7
|
@@ -39,7 +39,7 @@ async function upload(requests, appName, distribution) {
|
|
|
39
39
|
utils.logSuccess(`View this release in the Firebase console: ${release.firebaseConsoleUri}`);
|
|
40
40
|
utils.logSuccess(`Share this release with testers who have access: ${release.testingUri}`);
|
|
41
41
|
utils.logSuccess(`Download the release binary (link expires in 1 hour): ${release.binaryDownloadUri}`);
|
|
42
|
-
return uploadResponse.release
|
|
42
|
+
return uploadResponse.release;
|
|
43
43
|
}
|
|
44
44
|
catch (err) {
|
|
45
45
|
if ((0, error_1.getErrStatus)(err) === 404) {
|
|
@@ -50,7 +50,14 @@ function getAppName(options) {
|
|
|
50
50
|
}
|
|
51
51
|
return toAppName(options.app);
|
|
52
52
|
}
|
|
53
|
+
const APP_ID_FORMAT = /^\d+:\d+:(android|ios|web):[a-fA-F0-9]+$/;
|
|
54
|
+
function validateAppId(appId) {
|
|
55
|
+
if (!APP_ID_FORMAT.test(appId)) {
|
|
56
|
+
throw new error_1.FirebaseError(`Invalid Firebase app ID: ${appId}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
53
59
|
function toAppName(appId) {
|
|
60
|
+
validateAppId(appId);
|
|
54
61
|
return `projects/${appId.split(":")[1]}/apps/${appId}`;
|
|
55
62
|
}
|
|
56
63
|
function parseTestDevices(value, file = "") {
|
package/lib/apphosting/config.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_YAML_FILE = export
|
|
|
4
4
|
exports.discoverBackendRoot = discoverBackendRoot;
|
|
5
5
|
exports.listAppHostingFilesInPath = listAppHostingFilesInPath;
|
|
6
6
|
exports.load = load;
|
|
7
|
+
exports.getAppHostingConfiguration = getAppHostingConfiguration;
|
|
7
8
|
exports.store = store;
|
|
8
9
|
exports.findEnv = findEnv;
|
|
9
10
|
exports.upsertEnv = upsertEnv;
|
|
@@ -22,6 +23,7 @@ const yaml_1 = require("./yaml");
|
|
|
22
23
|
const logger_1 = require("../logger");
|
|
23
24
|
const csm = require("../gcp/secretManager");
|
|
24
25
|
const error_1 = require("../error");
|
|
26
|
+
const path_2 = require("path");
|
|
25
27
|
exports.APPHOSTING_BASE_YAML_FILE = "apphosting.yaml";
|
|
26
28
|
exports.APPHOSTING_EMULATORS_YAML_FILE = "apphosting.emulator.yaml";
|
|
27
29
|
exports.APPHOSTING_LOCAL_YAML_FILE = "apphosting.local.yaml";
|
|
@@ -64,6 +66,28 @@ function load(yamlPath) {
|
|
|
64
66
|
}
|
|
65
67
|
return yaml.parseDocument(raw);
|
|
66
68
|
}
|
|
69
|
+
const dynamicDispatch = exports;
|
|
70
|
+
async function getAppHostingConfiguration(backendDir) {
|
|
71
|
+
const appHostingConfigPaths = dynamicDispatch.listAppHostingFilesInPath(backendDir);
|
|
72
|
+
const fileNameToPathMap = Object.fromEntries(appHostingConfigPaths.map((path) => [(0, path_2.basename)(path), path]));
|
|
73
|
+
const output = yaml_1.AppHostingYamlConfig.empty();
|
|
74
|
+
const baseFilePath = fileNameToPathMap[exports.APPHOSTING_BASE_YAML_FILE];
|
|
75
|
+
const emulatorsFilePath = fileNameToPathMap[exports.APPHOSTING_EMULATORS_YAML_FILE];
|
|
76
|
+
const localFilePath = fileNameToPathMap[exports.APPHOSTING_LOCAL_YAML_FILE];
|
|
77
|
+
if (baseFilePath) {
|
|
78
|
+
const baseFile = await yaml_1.AppHostingYamlConfig.loadFromFile(baseFilePath);
|
|
79
|
+
output.merge(baseFile, false);
|
|
80
|
+
}
|
|
81
|
+
if (emulatorsFilePath) {
|
|
82
|
+
const emulatorsConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(emulatorsFilePath);
|
|
83
|
+
output.merge(emulatorsConfig, false);
|
|
84
|
+
}
|
|
85
|
+
if (localFilePath) {
|
|
86
|
+
const localYamlConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(localFilePath);
|
|
87
|
+
output.merge(localYamlConfig, true);
|
|
88
|
+
}
|
|
89
|
+
return output;
|
|
90
|
+
}
|
|
67
91
|
function store(yamlPath, document) {
|
|
68
92
|
(0, fs_1.writeFileSync)(yamlPath, document.toString());
|
|
69
93
|
}
|
|
@@ -94,7 +118,6 @@ function upsertEnv(document, env) {
|
|
|
94
118
|
}
|
|
95
119
|
envs.add(envYaml);
|
|
96
120
|
}
|
|
97
|
-
const dynamicDispatch = exports;
|
|
98
121
|
async function maybeAddSecretToYaml(secretName, fileName = exports.APPHOSTING_BASE_YAML_FILE) {
|
|
99
122
|
const backendRoot = dynamicDispatch.discoverBackendRoot(process.cwd());
|
|
100
123
|
let path;
|
|
@@ -97,7 +97,7 @@ async function distribute(appName, distribution, testCases, testDevices, release
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
const
|
|
100
|
+
const release = await (0, distribution_1.upload)(requests, appName, distribution);
|
|
101
101
|
if (aabInfo && !aabInfo.testCertificate) {
|
|
102
102
|
aabInfo = await requests.getAabInfo(appName);
|
|
103
103
|
if (aabInfo.testCertificate) {
|
|
@@ -110,17 +110,17 @@ async function distribute(appName, distribution, testCases, testDevices, release
|
|
|
110
110
|
`SHA-256 certificate fingerprint: ${aabInfo.testCertificate.hashSha256}`);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
await requests.updateReleaseNotes(
|
|
114
|
-
await requests.distribute(
|
|
113
|
+
await requests.updateReleaseNotes(release.name, releaseNotes);
|
|
114
|
+
await requests.distribute(release.name, testers, groups);
|
|
115
115
|
if (testDevices.length) {
|
|
116
116
|
utils.logBullet("starting automated test (note: this feature is in beta)");
|
|
117
117
|
const releaseTestPromises = [];
|
|
118
118
|
if (!testCases.length) {
|
|
119
|
-
releaseTestPromises.push(requests.createReleaseTest(
|
|
119
|
+
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices, undefined, loginCredential));
|
|
120
120
|
}
|
|
121
121
|
else {
|
|
122
122
|
for (const testCaseId of testCases) {
|
|
123
|
-
releaseTestPromises.push(requests.createReleaseTest(
|
|
123
|
+
releaseTestPromises.push(requests.createReleaseTest(release.name, testDevices, undefined, loginCredential, `${appName}/testCases/${testCaseId}`));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
const releaseTests = await Promise.all(releaseTestPromises);
|
|
@@ -8,7 +8,6 @@ const clc = require("colorette");
|
|
|
8
8
|
const parseTestFiles_1 = require("../apptesting/parseTestFiles");
|
|
9
9
|
const ora = require("ora");
|
|
10
10
|
const error_1 = require("../error");
|
|
11
|
-
const marked_1 = require("marked");
|
|
12
11
|
const client_1 = require("../appdistribution/client");
|
|
13
12
|
const distribution_1 = require("../appdistribution/distribution");
|
|
14
13
|
const options_parser_util_1 = require("../appdistribution/options-parser-util");
|
|
@@ -38,13 +37,13 @@ exports.command = new command_1.Command("apptesting:execute <release-binary-file
|
|
|
38
37
|
throw new error_1.FirebaseError("No tests found");
|
|
39
38
|
}
|
|
40
39
|
const invokeSpinner = ora("Requesting test execution");
|
|
41
|
-
let
|
|
42
|
-
let
|
|
40
|
+
let releaseTests;
|
|
41
|
+
let release;
|
|
43
42
|
try {
|
|
44
43
|
const client = new client_1.AppDistributionClient();
|
|
45
|
-
|
|
44
|
+
release = await (0, distribution_1.upload)(client, appName, new distribution_1.Distribution(target));
|
|
46
45
|
invokeSpinner.start();
|
|
47
|
-
|
|
46
|
+
releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices);
|
|
48
47
|
invokeSpinner.text = "Test execution requested";
|
|
49
48
|
invokeSpinner.succeed();
|
|
50
49
|
}
|
|
@@ -52,22 +51,22 @@ exports.command = new command_1.Command("apptesting:execute <release-binary-file
|
|
|
52
51
|
invokeSpinner.fail("Failed to request test execution");
|
|
53
52
|
throw ex;
|
|
54
53
|
}
|
|
55
|
-
logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(
|
|
56
|
-
logger_1.logger.info(
|
|
54
|
+
logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(releaseTests.length)}`));
|
|
55
|
+
logger_1.logger.info(`View progress and results in the Firebase Console:\n${release.firebaseConsoleUri}`);
|
|
57
56
|
});
|
|
58
57
|
function pluralizeTests(numTests) {
|
|
59
58
|
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
60
59
|
}
|
|
61
60
|
async function invokeTests(client, releaseName, testDefs, devices) {
|
|
62
61
|
try {
|
|
63
|
-
const
|
|
62
|
+
const releaseTests = [];
|
|
64
63
|
for (const testDef of testDefs) {
|
|
65
64
|
const aiInstructions = {
|
|
66
65
|
steps: testDef.testCase.steps,
|
|
67
66
|
};
|
|
68
|
-
|
|
67
|
+
releaseTests.push(await client.createReleaseTest(releaseName, devices, aiInstructions, undefined, undefined, testDef.testCase.displayName));
|
|
69
68
|
}
|
|
70
|
-
return
|
|
69
|
+
return releaseTests;
|
|
71
70
|
}
|
|
72
71
|
catch (err) {
|
|
73
72
|
throw new error_1.FirebaseError("Test invocation failed", { original: (0, error_1.getError)(err) });
|
|
@@ -8,7 +8,6 @@ const requireAuth_1 = require("../requireAuth");
|
|
|
8
8
|
const requirePermissions_1 = require("../requirePermissions");
|
|
9
9
|
const ensureApis_1 = require("../dataconnect/ensureApis");
|
|
10
10
|
const permissionsSetup_1 = require("../gcp/cloudsql/permissionsSetup");
|
|
11
|
-
const permissions_1 = require("../gcp/cloudsql/permissions");
|
|
12
11
|
const schemaMigration_1 = require("../dataconnect/schemaMigration");
|
|
13
12
|
const connect_1 = require("../gcp/cloudsql/connect");
|
|
14
13
|
const load_1 = require("../dataconnect/load");
|
|
@@ -33,9 +32,9 @@ exports.command = new command_1.Command("dataconnect:sql:setup")
|
|
|
33
32
|
if (!instanceId) {
|
|
34
33
|
throw new error_1.FirebaseError("dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId");
|
|
35
34
|
}
|
|
36
|
-
const { serviceName, instanceName, databaseId } = (0, schemaMigration_1.getIdentifiers)((0, types_1.mainSchema)(serviceInfo.schemas));
|
|
37
|
-
await (0, schemaMigration_1.ensureServiceIsConnectedToCloudSql)(serviceName, instanceName, databaseId, true);
|
|
35
|
+
const { serviceName, instanceName, databaseId, schemaName } = (0, schemaMigration_1.getIdentifiers)((0, types_1.mainSchema)(serviceInfo.schemas));
|
|
36
|
+
await (0, schemaMigration_1.ensureServiceIsConnectedToCloudSql)(serviceName, instanceName, databaseId, true, schemaName);
|
|
38
37
|
await (0, connect_1.setupIAMUsers)(instanceId, options);
|
|
39
|
-
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId,
|
|
38
|
+
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, schemaName, options);
|
|
40
39
|
await (0, permissionsSetup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options);
|
|
41
40
|
});
|
|
@@ -83,7 +83,7 @@ exports.command = new command_1.Command("dataconnect:sql:shell")
|
|
|
83
83
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
84
84
|
await (0, ensureApis_1.ensureApis)(projectId);
|
|
85
85
|
const serviceInfo = await (0, load_1.pickOneService)(projectId, options.config, options.service, options.location);
|
|
86
|
-
const { instanceId, databaseId } = (0, schemaMigration_1.getIdentifiers)((0, types_1.mainSchema)(serviceInfo.schemas));
|
|
86
|
+
const { instanceId, databaseId, schemaName } = (0, schemaMigration_1.getIdentifiers)((0, types_1.mainSchema)(serviceInfo.schemas));
|
|
87
87
|
const { user: username } = await (0, connect_1.getIAMUser)(options);
|
|
88
88
|
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
|
|
89
89
|
const connectionName = instance.connectionName;
|
|
@@ -104,6 +104,7 @@ exports.command = new command_1.Command("dataconnect:sql:shell")
|
|
|
104
104
|
database: databaseId,
|
|
105
105
|
});
|
|
106
106
|
const conn = await pool.connect();
|
|
107
|
+
await conn.query(`SET search_path TO "${schemaName}"`);
|
|
107
108
|
logger_1.logger.info(`Logged in as ${username}`);
|
|
108
109
|
logger_1.logger.info(clc.cyan("Welcome to Data Connect Cloud SQL Shell"));
|
|
109
110
|
logger_1.logger.info(clc.gray("Type your your SQL query or '.exit' to quit, queries should end with ';' or add empty line to execute."));
|
package/lib/commands/index.js
CHANGED
|
@@ -247,18 +247,16 @@ function load(client) {
|
|
|
247
247
|
client.dataconnect.compile = loadCommand("dataconnect-compile");
|
|
248
248
|
client.dataconnect.sdk = {};
|
|
249
249
|
client.dataconnect.sdk.generate = loadCommand("dataconnect-sdk-generate");
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
client.studio.export = loadCommand("studio-export");
|
|
253
|
-
}
|
|
250
|
+
client.studio = {};
|
|
251
|
+
client.studio.export = loadCommand("studio-export");
|
|
254
252
|
client.target = loadCommand("target");
|
|
255
253
|
client.target.apply = loadCommand("target-apply");
|
|
256
254
|
client.target.clear = loadCommand("target-clear");
|
|
257
255
|
client.target.remove = loadCommand("target-remove");
|
|
258
256
|
client.use = loadCommand("use");
|
|
257
|
+
client.apptesting = {};
|
|
258
|
+
client.apptesting.execute = loadCommand("apptesting");
|
|
259
259
|
if (experiments.isEnabled("apptesting")) {
|
|
260
|
-
client.apptesting = {};
|
|
261
|
-
client.apptesting.execute = loadCommand("apptesting");
|
|
262
260
|
client.apptesting.wata = loadCommand("apptesting-wata");
|
|
263
261
|
}
|
|
264
262
|
const t1 = process.hrtime.bigint();
|
|
@@ -5,17 +5,35 @@ const command_1 = require("../command");
|
|
|
5
5
|
const logger_1 = require("../logger");
|
|
6
6
|
const migrate_1 = require("../firebase_studio/migrate");
|
|
7
7
|
const path = require("path");
|
|
8
|
-
const experiments = require("../experiments");
|
|
9
8
|
const error_1 = require("../error");
|
|
9
|
+
const unzip_1 = require("../unzip");
|
|
10
|
+
const fs = require("fs");
|
|
10
11
|
exports.command = new command_1.Command("studio:export <path>")
|
|
11
|
-
.description("Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download.")
|
|
12
|
-
.option("--no-start-
|
|
12
|
+
.description("Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download, or directly on the downloaded zip file.")
|
|
13
|
+
.option("--no-start-antigravity", "skip starting the Antigravity IDE after migration")
|
|
13
14
|
.action(async (exportPath, options) => {
|
|
14
|
-
experiments.assertEnabled("studioexport", "export Studio apps");
|
|
15
15
|
if (!exportPath) {
|
|
16
16
|
throw new error_1.FirebaseError("Must specify a path for migration.", { exit: 1 });
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let rootPath = path.resolve(exportPath);
|
|
19
|
+
if (fs.existsSync(rootPath) && fs.statSync(rootPath).isFile() && rootPath.endsWith(".zip")) {
|
|
20
|
+
logger_1.logger.info(`⏳ Unzipping ${rootPath}...`);
|
|
21
|
+
const parsedPath = path.parse(rootPath);
|
|
22
|
+
let extractDirName = parsedPath.name;
|
|
23
|
+
if (!extractDirName || extractDirName === ".") {
|
|
24
|
+
extractDirName = "studio-export";
|
|
25
|
+
}
|
|
26
|
+
const extractPath = path.join(parsedPath.dir, extractDirName);
|
|
27
|
+
await (0, unzip_1.unzip)(rootPath, extractPath);
|
|
28
|
+
const extractedItems = fs.readdirSync(extractPath);
|
|
29
|
+
if (extractedItems.length === 1 &&
|
|
30
|
+
fs.statSync(path.join(extractPath, extractedItems[0])).isDirectory()) {
|
|
31
|
+
rootPath = path.join(extractPath, extractedItems[0]);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
rootPath = extractPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
logger_1.logger.info(`⏳ Exporting Studio app from ${rootPath} to Antigravity...`);
|
|
20
38
|
await (0, migrate_1.migrate)(rootPath, options);
|
|
21
39
|
});
|
package/lib/dataconnect/load.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.loadAll = loadAll;
|
|
|
6
6
|
exports.load = load;
|
|
7
7
|
exports.readFirebaseJson = readFirebaseJson;
|
|
8
8
|
exports.readDataConnectYaml = readDataConnectYaml;
|
|
9
|
+
exports.inferClientCache = inferClientCache;
|
|
9
10
|
exports.readConnectorYaml = readConnectorYaml;
|
|
10
11
|
exports.readGQLFiles = readGQLFiles;
|
|
11
12
|
exports.squashGraphQL = squashGraphQL;
|
|
@@ -28,7 +29,7 @@ async function pickOneService(projectId, config, service, location) {
|
|
|
28
29
|
async function pickServices(projectId, config, serviceId, location) {
|
|
29
30
|
const serviceInfos = await loadAll(projectId, config);
|
|
30
31
|
if (serviceInfos.length === 0) {
|
|
31
|
-
throw new error_1.FirebaseError("No Data Connect services found in firebase.json." +
|
|
32
|
+
throw new error_1.FirebaseError("No Data Connect services found in firebase.json. " +
|
|
32
33
|
`\nYou can run ${clc.bold("firebase init dataconnect")} to add a Data Connect service.`);
|
|
33
34
|
}
|
|
34
35
|
const matchingServices = serviceInfos.filter((i) => (!serviceId || i.dataConnectYaml.serviceId === serviceId) &&
|
|
@@ -63,6 +64,7 @@ async function load(projectId, config, sourceDirectory) {
|
|
|
63
64
|
const connectorDir = path.join(resolvedDir, dir);
|
|
64
65
|
const connectorYaml = await readConnectorYaml(connectorDir);
|
|
65
66
|
const connectorGqls = await readGQLFiles(connectorDir);
|
|
67
|
+
const clientCache = inferClientCache(connectorYaml);
|
|
66
68
|
return {
|
|
67
69
|
directory: connectorDir,
|
|
68
70
|
connectorYaml,
|
|
@@ -71,6 +73,7 @@ async function load(projectId, config, sourceDirectory) {
|
|
|
71
73
|
source: {
|
|
72
74
|
files: connectorGqls,
|
|
73
75
|
},
|
|
76
|
+
client_cache: clientCache,
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
}));
|
|
@@ -122,6 +125,26 @@ function validateDataConnectYaml(unvalidated) {
|
|
|
122
125
|
}
|
|
123
126
|
return unvalidated;
|
|
124
127
|
}
|
|
128
|
+
function inferClientCache(connectorYaml) {
|
|
129
|
+
const platforms = [
|
|
130
|
+
connectorYaml.generate?.javascriptSdk,
|
|
131
|
+
connectorYaml.generate?.swiftSdk,
|
|
132
|
+
connectorYaml.generate?.kotlinSdk,
|
|
133
|
+
connectorYaml.generate?.dartSdk,
|
|
134
|
+
];
|
|
135
|
+
for (const sdk of platforms) {
|
|
136
|
+
if (sdk) {
|
|
137
|
+
const sdkList = Array.isArray(sdk) ? sdk : [sdk];
|
|
138
|
+
if (sdkList.some((s) => s.clientCache)) {
|
|
139
|
+
return {
|
|
140
|
+
strict_validation_enabled: true,
|
|
141
|
+
entity_id_included: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
125
148
|
async function readConnectorYaml(sourceDirectory) {
|
|
126
149
|
const file = await (0, utils_1.readFileFromDirectory)(sourceDirectory, "connector.yaml");
|
|
127
150
|
const connectorYaml = await (0, utils_1.wrappedSafeLoad)(file.source);
|
|
@@ -25,10 +25,10 @@ const errors = require("./errors");
|
|
|
25
25
|
const provisionCloudSql_1 = require("./provisionCloudSql");
|
|
26
26
|
const requireAuth_1 = require("../requireAuth");
|
|
27
27
|
const cloudbilling_1 = require("../gcp/cloudbilling");
|
|
28
|
-
async function setupSchemaIfNecessary(instanceId, databaseId, options) {
|
|
28
|
+
async function setupSchemaIfNecessary(instanceId, databaseId, schemaName, options) {
|
|
29
29
|
try {
|
|
30
30
|
await (0, connect_1.setupIAMUsers)(instanceId, options);
|
|
31
|
-
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId,
|
|
31
|
+
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, schemaName, options);
|
|
32
32
|
switch (schemaInfo.setupStatus) {
|
|
33
33
|
case permissionsSetup_1.SchemaSetupStatus.BrownField:
|
|
34
34
|
case permissionsSetup_1.SchemaSetupStatus.GreenField:
|
|
@@ -50,8 +50,8 @@ async function diffSchema(options, schema, schemaValidation) {
|
|
|
50
50
|
let validationMode = schemaValidation ?? "STRICT";
|
|
51
51
|
setSchemaValidationMode(schema, validationMode);
|
|
52
52
|
displayStartSchemaDiff(validationMode);
|
|
53
|
-
const { serviceName, instanceName, databaseId, instanceId } = getIdentifiers(schema);
|
|
54
|
-
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
|
|
53
|
+
const { serviceName, instanceName, databaseId, instanceId, schemaName } = getIdentifiers(schema);
|
|
54
|
+
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false, schemaName);
|
|
55
55
|
let incompatible = undefined;
|
|
56
56
|
try {
|
|
57
57
|
await (0, client_1.upsertSchema)(schema, true);
|
|
@@ -114,8 +114,8 @@ async function migrateSchema(args) {
|
|
|
114
114
|
setSchemaValidationMode(schema, validationMode);
|
|
115
115
|
displayStartSchemaDiff(validationMode);
|
|
116
116
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
117
|
-
const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
|
|
118
|
-
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true);
|
|
117
|
+
const { serviceName, instanceId, instanceName, databaseId, schemaName } = getIdentifiers(schema);
|
|
118
|
+
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, true, schemaName);
|
|
119
119
|
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
|
|
120
120
|
if (existingInstance.state === "PENDING_CREATE") {
|
|
121
121
|
if (stats) {
|
|
@@ -134,7 +134,7 @@ async function migrateSchema(args) {
|
|
|
134
134
|
(0, utils_1.logLabeledWarning)("dataconnect", `Skip SQL schema migration because Cloud SQL is still being created`);
|
|
135
135
|
return [];
|
|
136
136
|
}
|
|
137
|
-
await setupSchemaIfNecessary(instanceId, databaseId, options);
|
|
137
|
+
await setupSchemaIfNecessary(instanceId, databaseId, schemaName, options);
|
|
138
138
|
let diffs = [];
|
|
139
139
|
try {
|
|
140
140
|
await (0, client_1.upsertSchema)(schema, validateOnly);
|
|
@@ -168,6 +168,7 @@ async function migrateSchema(args) {
|
|
|
168
168
|
options,
|
|
169
169
|
databaseId,
|
|
170
170
|
instanceId,
|
|
171
|
+
schemaName,
|
|
171
172
|
incompatibleSchemaError: incompatible,
|
|
172
173
|
choice: migrationMode,
|
|
173
174
|
});
|
|
@@ -202,6 +203,7 @@ async function migrateSchema(args) {
|
|
|
202
203
|
options,
|
|
203
204
|
databaseId,
|
|
204
205
|
instanceId,
|
|
206
|
+
schemaName,
|
|
205
207
|
incompatibleSchemaError: incompatible,
|
|
206
208
|
choice: migrationMode,
|
|
207
209
|
});
|
|
@@ -242,13 +244,13 @@ async function upsertSecondarySchema(args) {
|
|
|
242
244
|
async function grantRoleToUserInSchema(options, schema) {
|
|
243
245
|
const role = options.role;
|
|
244
246
|
const email = options.email;
|
|
245
|
-
const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
|
|
246
|
-
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false);
|
|
247
|
-
const schemaSetupStatus = await setupSchemaIfNecessary(instanceId, databaseId, options);
|
|
247
|
+
const { serviceName, instanceId, instanceName, databaseId, schemaName } = getIdentifiers(schema);
|
|
248
|
+
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, false, schemaName);
|
|
249
|
+
const schemaSetupStatus = await setupSchemaIfNecessary(instanceId, databaseId, schemaName, options);
|
|
248
250
|
if (schemaSetupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField && role === "owner") {
|
|
249
251
|
throw new error_1.FirebaseError(`Owner rule isn't available in ${schemaSetupStatus} databases. If you would like Data Connect to manage and own your database schema, run 'firebase dataconnect:sql:setup'`);
|
|
250
252
|
}
|
|
251
|
-
await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, role, email);
|
|
253
|
+
await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, role, email, schemaName);
|
|
252
254
|
}
|
|
253
255
|
function diffsEqual(x, y) {
|
|
254
256
|
if (x.length !== y.length) {
|
|
@@ -280,11 +282,13 @@ function getIdentifiers(schema) {
|
|
|
280
282
|
throw new error_1.FirebaseError("Data Connect schema must have a postgres datasource with a CloudSQL instance.");
|
|
281
283
|
}
|
|
282
284
|
const instanceId = instanceName.split("/").pop();
|
|
285
|
+
const schemaName = postgresDatasource?.postgresql?.schema || permissions_1.DEFAULT_SCHEMA;
|
|
283
286
|
const serviceName = serviceNameFromSchema(schema);
|
|
284
287
|
return {
|
|
285
288
|
databaseId,
|
|
286
289
|
instanceId,
|
|
287
290
|
instanceName,
|
|
291
|
+
schemaName,
|
|
288
292
|
serviceName,
|
|
289
293
|
};
|
|
290
294
|
}
|
|
@@ -299,7 +303,7 @@ function suggestedCommand(serviceName, invalidConnectorNames) {
|
|
|
299
303
|
return `firebase deploy --only ${onlys}`;
|
|
300
304
|
}
|
|
301
305
|
async function handleIncompatibleSchemaError(args) {
|
|
302
|
-
const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args;
|
|
306
|
+
const { incompatibleSchemaError, options, instanceId, databaseId, schemaName, choice } = args;
|
|
303
307
|
const commandsToExecute = incompatibleSchemaError.diffs.filter((d) => {
|
|
304
308
|
switch (choice) {
|
|
305
309
|
case "all":
|
|
@@ -319,26 +323,29 @@ async function handleIncompatibleSchemaError(args) {
|
|
|
319
323
|
Please ask a user with 'roles/cloudsql.admin' to apply the following commands.\n
|
|
320
324
|
${diffsToString(commandsToExecuteBySuperUser)}`);
|
|
321
325
|
}
|
|
322
|
-
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId,
|
|
326
|
+
const schemaInfo = await (0, permissionsSetup_1.getSchemaMetadata)(instanceId, databaseId, schemaName, options);
|
|
323
327
|
if (schemaInfo.setupStatus !== permissionsSetup_1.SchemaSetupStatus.GreenField) {
|
|
324
328
|
throw new error_1.FirebaseError(`Brownfield database are protected from SQL changes by Data Connect.\n` +
|
|
325
329
|
`You can use the SQL diff generated by 'firebase dataconnect:sql:diff' to assist you in applying the required changes to your CloudSQL database. Connector deployment will succeed when there is no required diff changes.\n` +
|
|
326
330
|
`If you would like Data Connect to manage your database schema, run 'firebase dataconnect:sql:setup'`);
|
|
327
331
|
}
|
|
328
|
-
if (!(await (0, permissionsSetup_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId), (await (0, connect_1.getIAMUser)(options)).user))) {
|
|
332
|
+
if (!(await (0, permissionsSetup_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId, schemaName), (await (0, connect_1.getIAMUser)(options)).user))) {
|
|
329
333
|
if (!userIsCSQLAdmin) {
|
|
330
334
|
throw new error_1.FirebaseError(`Command aborted. Only users granted firebaseowner SQL role can run migrations.`);
|
|
331
335
|
}
|
|
332
336
|
const account = (await (0, requireAuth_1.requireAuth)(options));
|
|
333
337
|
(0, utils_1.logLabeledBullet)("dataconnect", `Granting firebaseowner role to myself ${account}...`);
|
|
334
|
-
await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, "owner", account);
|
|
338
|
+
await (0, permissionsSetup_1.grantRoleTo)(options, instanceId, databaseId, "owner", account, schemaName);
|
|
335
339
|
}
|
|
336
340
|
if (commandsToExecuteBySuperUser.length) {
|
|
337
341
|
(0, utils_1.logLabeledBullet)("dataconnect", `Executing admin SQL commands as superuser...`);
|
|
338
342
|
await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, commandsToExecuteBySuperUser.map((d) => d.sql), false);
|
|
339
343
|
}
|
|
340
344
|
if (commandsToExecuteByOwner.length) {
|
|
341
|
-
await (0, connect_1.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [
|
|
345
|
+
await (0, connect_1.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [
|
|
346
|
+
`SET ROLE "${(0, permissions_1.firebaseowner)(databaseId, schemaName)}"`,
|
|
347
|
+
...commandsToExecuteByOwner.map((d) => d.sql),
|
|
348
|
+
], false);
|
|
342
349
|
return incompatibleSchemaError.diffs;
|
|
343
350
|
}
|
|
344
351
|
}
|
|
@@ -421,7 +428,7 @@ function displayInvalidConnectors(invalidConnectors) {
|
|
|
421
428
|
(0, utils_1.logLabeledWarning)("dataconnect", `The schema you are deploying is incompatible with the following existing connectors: ${clc.bold(connectorIds)}.`);
|
|
422
429
|
(0, utils_1.logLabeledWarning)("dataconnect", `This is a ${clc.red("breaking")} change and may break existing apps.`);
|
|
423
430
|
}
|
|
424
|
-
async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, linkIfNotConnected) {
|
|
431
|
+
async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId, linkIfNotConnected, schemaName) {
|
|
425
432
|
let currentSchema = await (0, client_1.getSchema)(serviceName);
|
|
426
433
|
let postgresql = currentSchema?.datasources?.find((d) => d.postgresql)?.postgresql;
|
|
427
434
|
if (currentSchema?.reconciling &&
|
|
@@ -478,6 +485,7 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, dat
|
|
|
478
485
|
try {
|
|
479
486
|
postgresql.schemaValidation = "STRICT";
|
|
480
487
|
postgresql.database = databaseId;
|
|
488
|
+
postgresql.schema = schemaName;
|
|
481
489
|
postgresql.cloudSql = { instance: instanceName };
|
|
482
490
|
await (0, client_1.upsertSchema)(currentSchema, false);
|
|
483
491
|
}
|
package/lib/dataconnect/types.js
CHANGED
|
@@ -16,6 +16,7 @@ function toDatasource(projectId, locationId, ds) {
|
|
|
16
16
|
return {
|
|
17
17
|
postgresql: {
|
|
18
18
|
database: ds.postgresql.database,
|
|
19
|
+
schema: ds.postgresql.schema,
|
|
19
20
|
cloudSql: {
|
|
20
21
|
instance: `projects/${projectId}/locations/${locationId}/instances/${ds.postgresql.cloudSql.instanceId}`,
|
|
21
22
|
},
|
|
@@ -226,18 +226,33 @@ function toBackend(build, paramValues) {
|
|
|
226
226
|
r.resolveInts(bkEndpoint, bdEndpoint, "timeoutSeconds", "maxInstances", "minInstances", "concurrency");
|
|
227
227
|
proto.convertIfPresent(bkEndpoint, bdEndpoint, "cpu", (0, functional_1.nullsafeVisitor)((cpu) => (cpu === "gcf_gen1" ? cpu : r.resolveInt(cpu))));
|
|
228
228
|
if (bdEndpoint.vpc) {
|
|
229
|
-
|
|
230
|
-
if (bdEndpoint.vpc.connector &&
|
|
231
|
-
|
|
229
|
+
bkEndpoint.vpc = {};
|
|
230
|
+
if (typeof bdEndpoint.vpc.connector !== "undefined" && bdEndpoint.vpc.connector !== null) {
|
|
231
|
+
const connector = params.resolveString(bdEndpoint.vpc.connector, paramValues);
|
|
232
|
+
bkEndpoint.vpc.connector =
|
|
233
|
+
connector.includes("/") || connector === ""
|
|
234
|
+
? connector
|
|
235
|
+
: `projects/${bdEndpoint.project}/locations/${region}/connectors/${connector}`;
|
|
232
236
|
}
|
|
233
|
-
bkEndpoint.vpc = { connector: bdEndpoint.vpc.connector };
|
|
234
237
|
if (bdEndpoint.vpc.egressSettings) {
|
|
235
|
-
const
|
|
236
|
-
if (!backend.
|
|
237
|
-
throw new error_1.FirebaseError(`Value "${
|
|
238
|
-
"egress setting. Valid values are PRIVATE_RANGES_ONLY and ALL_TRAFFIC");
|
|
238
|
+
const egress = params.resolveString(bdEndpoint.vpc.egressSettings, paramValues);
|
|
239
|
+
if (!backend.AllVpcEgressSettings.includes(egress)) {
|
|
240
|
+
throw new error_1.FirebaseError(`Value "${egress}" is an invalid egress setting.`);
|
|
239
241
|
}
|
|
240
|
-
bkEndpoint.vpc.egressSettings =
|
|
242
|
+
bkEndpoint.vpc.egressSettings = egress;
|
|
243
|
+
}
|
|
244
|
+
if (bdEndpoint.vpc.networkInterfaces) {
|
|
245
|
+
bkEndpoint.vpc.networkInterfaces = bdEndpoint.vpc.networkInterfaces.map((ni) => {
|
|
246
|
+
const resolved = {};
|
|
247
|
+
if (ni.network)
|
|
248
|
+
resolved.network = params.resolveString(ni.network, paramValues);
|
|
249
|
+
if (ni.subnetwork)
|
|
250
|
+
resolved.subnetwork = params.resolveString(ni.subnetwork, paramValues);
|
|
251
|
+
if (ni.tags) {
|
|
252
|
+
resolved.tags = ni.tags.map((tag) => params.resolveString(tag, paramValues));
|
|
253
|
+
}
|
|
254
|
+
return resolved;
|
|
255
|
+
});
|
|
241
256
|
}
|
|
242
257
|
}
|
|
243
258
|
else if (bdEndpoint.vpc === null) {
|
|
@@ -88,10 +88,16 @@ function assertBuildEndpoint(ep, id) {
|
|
|
88
88
|
});
|
|
89
89
|
if (ep.vpc) {
|
|
90
90
|
(0, parsing_1.assertKeyTypes)(prefix + ".vpc", ep.vpc, {
|
|
91
|
-
connector: "string",
|
|
91
|
+
connector: "string?",
|
|
92
92
|
egressSettings: (setting) => setting === null || build.AllVpcEgressSettings.includes(setting),
|
|
93
|
+
networkInterfaces: "array?",
|
|
93
94
|
});
|
|
94
|
-
|
|
95
|
+
if (!ep.vpc.connector && !ep.vpc.networkInterfaces) {
|
|
96
|
+
throw new error_1.FirebaseError(`VPC settings on ${id} must specify either 'connector' or 'networkInterfaces'`);
|
|
97
|
+
}
|
|
98
|
+
if (ep.vpc.connector && ep.vpc.networkInterfaces) {
|
|
99
|
+
throw new error_1.FirebaseError(`VPC settings on ${id} cannot specify both 'connector' and 'networkInterfaces'`);
|
|
100
|
+
}
|
|
95
101
|
}
|
|
96
102
|
let triggerCount = 0;
|
|
97
103
|
if (ep.httpsTrigger) {
|
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getLocalAppHostingConfiguration = getLocalAppHostingConfiguration;
|
|
4
|
-
const path_1 = require("path");
|
|
5
4
|
const config_1 = require("../../apphosting/config");
|
|
6
|
-
const yaml_1 = require("../../apphosting/yaml");
|
|
7
5
|
async function getLocalAppHostingConfiguration(backendDir) {
|
|
8
|
-
|
|
9
|
-
const fileNameToPathMap = Object.fromEntries(appHostingConfigPaths.map((path) => [(0, path_1.basename)(path), path]));
|
|
10
|
-
const output = yaml_1.AppHostingYamlConfig.empty();
|
|
11
|
-
const baseFilePath = fileNameToPathMap[config_1.APPHOSTING_BASE_YAML_FILE];
|
|
12
|
-
const emulatorsFilePath = fileNameToPathMap[config_1.APPHOSTING_EMULATORS_YAML_FILE];
|
|
13
|
-
const localFilePath = fileNameToPathMap[config_1.APPHOSTING_LOCAL_YAML_FILE];
|
|
14
|
-
if (baseFilePath) {
|
|
15
|
-
const baseFile = await yaml_1.AppHostingYamlConfig.loadFromFile(baseFilePath);
|
|
16
|
-
output.merge(baseFile, false);
|
|
17
|
-
}
|
|
18
|
-
if (emulatorsFilePath) {
|
|
19
|
-
const emulatorsConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(emulatorsFilePath);
|
|
20
|
-
output.merge(emulatorsConfig, false);
|
|
21
|
-
}
|
|
22
|
-
if (localFilePath) {
|
|
23
|
-
const localYamlConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(localFilePath);
|
|
24
|
-
output.merge(localYamlConfig, true);
|
|
25
|
-
}
|
|
26
|
-
return output;
|
|
6
|
+
return (0, config_1.getAppHostingConfiguration)(backendDir);
|
|
27
7
|
}
|
|
@@ -753,7 +753,8 @@ async function exportEmulatorData(exportPath, options, initiatedBy) {
|
|
|
753
753
|
}
|
|
754
754
|
utils.logBullet(`Exporting data to: ${exportAbsPath}`);
|
|
755
755
|
try {
|
|
756
|
-
|
|
756
|
+
const targets = filterEmulatorTargets(options);
|
|
757
|
+
await hubClient.postExport({ path: exportAbsPath, initiatedBy, targets });
|
|
757
758
|
}
|
|
758
759
|
catch (e) {
|
|
759
760
|
throw new error_1.FirebaseError("Export request failed, see emulator logs for more information.", {
|
package/lib/emulator/hub.js
CHANGED
|
@@ -71,11 +71,13 @@ class EmulatorHub extends ExpressBasedEmulator_1.ExpressBasedEmulator {
|
|
|
71
71
|
}
|
|
72
72
|
const path = req.body.path;
|
|
73
73
|
const initiatedBy = req.body.initiatedBy || "unknown";
|
|
74
|
+
const targets = req.body.targets;
|
|
74
75
|
utils.logLabeledBullet("emulators", `Received export request. Exporting data to ${path}.`);
|
|
75
76
|
try {
|
|
76
77
|
await new hubExport_1.HubExport(this.args.projectId, {
|
|
77
78
|
path,
|
|
78
79
|
initiatedBy,
|
|
80
|
+
targets,
|
|
79
81
|
}).exportAll();
|
|
80
82
|
utils.logLabeledSuccess("emulators", "Export complete.");
|
|
81
83
|
res.status(200).send({
|