firebase-tools
Version:
Command-Line Interface for Firebase
298 lines (297 loc) • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataConnectEmulatorClient = exports.DataConnectEmulator = exports.dataConnectEmulatorEvents = void 0;
const childProcess = require("child_process");
const pg = require("pg");
const events_1 = require("events");
const clc = require("colorette");
const path = require("path");
const api_1 = require("../api");
const constants_1 = require("./constants");
const downloadableEmulators_1 = require("./downloadableEmulators");
const types_1 = require("./types");
const error_1 = require("../error");
const emulatorLogger_1 = require("./emulatorLogger");
const types_2 = require("../dataconnect/types");
const portUtils_1 = require("./portUtils");
const registry_1 = require("./registry");
const logger_1 = require("../logger");
const load_1 = require("../dataconnect/load");
const pgliteServer_1 = require("./dataconnect/pgliteServer");
const controller_1 = require("./controller");
const utils_1 = require("../utils");
const ensureApiEnabled_1 = require("../ensureApiEnabled");
const defaultCredentials_1 = require("../defaultCredentials");
exports.dataConnectEmulatorEvents = new events_1.EventEmitter();
class DataConnectEmulator {
constructor(args) {
this.args = args;
this.usingExistingEmulator = false;
this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.DATACONNECT);
this.emulatorClient = new DataConnectEmulatorClient();
}
async start() {
var _a, _b, _c;
let resolvedConfigDir;
try {
resolvedConfigDir = this.args.config.path(this.args.configDir);
const info = await DataConnectEmulator.build({
configDir: resolvedConfigDir,
account: this.args.account,
});
if ((0, types_2.requiresVector)(info.metadata)) {
if (constants_1.Constants.isDemoProject(this.args.projectId)) {
this.logger.logLabeled("WARN", "dataconnect", "Detected a 'demo-' project, but vector embeddings require a real project. Operations that use vector_embed will fail.");
}
else {
await (0, ensureApiEnabled_1.ensure)(this.args.projectId, (0, api_1.vertexAIOrigin)(), "dataconnect", true);
this.logger.logLabeled("WARN", "dataconnect", "Operations that use vector_embed will make calls to production Vertex AI");
}
}
}
catch (err) {
this.logger.log("DEBUG", `'fdc build' failed with error: ${err.message}`);
}
const env = await DataConnectEmulator.getEnv(this.args.account, this.args.extraEnv);
await (0, downloadableEmulators_1.start)(types_1.Emulators.DATACONNECT, {
auto_download: this.args.auto_download,
listen: (0, portUtils_1.listenSpecsToString)(this.args.listen),
config_dir: resolvedConfigDir,
enable_output_schema_extensions: this.args.enable_output_schema_extensions,
enable_output_generated_sdk: this.args.enable_output_generated_sdk,
}, env);
this.usingExistingEmulator = false;
if (this.args.autoconnectToPostgres) {
const info = await (0, load_1.load)(this.args.projectId, this.args.config, this.args.configDir);
const dbId = ((_a = info.dataConnectYaml.schema.datasource.postgresql) === null || _a === void 0 ? void 0 : _a.database) || "postgres";
const serviceId = info.dataConnectYaml.serviceId;
const pgPort = (_b = this.args.postgresListen) === null || _b === void 0 ? void 0 : _b[0].port;
const pgHost = (_c = this.args.postgresListen) === null || _c === void 0 ? void 0 : _c[0].address;
let connStr = (0, api_1.dataConnectLocalConnString)();
if (connStr) {
this.logger.logLabeled("INFO", "dataconnect", `FIREBASE_DATACONNECT_POSTGRESQL_STRING is set to ${clc.bold(connStr)} - using that instead of starting a new database`);
}
else if (pgHost && pgPort) {
let dataDirectory = this.args.config.get("emulators.dataconnect.dataDir");
if (dataDirectory) {
dataDirectory = this.args.config.path(dataDirectory);
}
const postgresDumpPath = this.args.importPath
? path.join(this.args.importPath, "postgres.tar.gz")
: undefined;
this.postgresServer = new pgliteServer_1.PostgresServer({
dataDirectory,
importPath: postgresDumpPath,
debug: this.args.debug,
});
const server = await this.postgresServer.createPGServer(pgHost, pgPort);
const connectableHost = (0, utils_1.connectableHostname)(pgHost);
connStr = `postgres://${connectableHost}:${pgPort}/${dbId}?sslmode=disable`;
server.on("error", (err) => {
if (err instanceof error_1.FirebaseError) {
this.logger.logLabeled("ERROR", "Data Connect", `${err}`);
}
else {
this.logger.logLabeled("ERROR", "dataconnect", `Postgres threw an unexpected error, shutting down the Data Connect emulator: ${err}`);
}
void (0, controller_1.cleanShutdown)();
});
this.logger.logLabeled("INFO", "dataconnect", `Started up Postgres server, listening on ${JSON.stringify(server.address())}`);
}
await this.connectToPostgres(new URL(connStr), dbId, serviceId);
}
return;
}
async connect() {
const emuInfo = await this.emulatorClient.getInfo();
if (!emuInfo) {
this.logger.logLabeled("ERROR", "dataconnect", "Could not connect to Data Connect emulator. Check dataconnect-debug.log for more details.");
return Promise.reject();
}
return Promise.resolve();
}
async stop() {
if (this.usingExistingEmulator) {
this.logger.logLabeled("INFO", "dataconnect", "Skipping cleanup of Data Connect emulator, as it was not started by this process.");
return;
}
if (this.postgresServer) {
await this.postgresServer.stop();
}
return (0, downloadableEmulators_1.stop)(types_1.Emulators.DATACONNECT);
}
getInfo() {
return {
name: this.getName(),
listen: this.args.listen,
host: this.args.listen[0].address,
port: this.args.listen[0].port,
pid: (0, downloadableEmulators_1.getPID)(types_1.Emulators.DATACONNECT),
timeout: 10000,
};
}
getName() {
return types_1.Emulators.DATACONNECT;
}
getVersion() {
return (0, downloadableEmulators_1.getDownloadDetails)(types_1.Emulators.DATACONNECT).version;
}
async clearData() {
if (this.postgresServer) {
await this.postgresServer.clearDb();
}
else {
const conn = new pg.Client((0, api_1.dataConnectLocalConnString)());
await conn.query(pgliteServer_1.TRUNCATE_TABLES_SQL);
await conn.end();
}
}
async exportData(exportPath) {
if (this.postgresServer) {
await this.postgresServer.exportData(path.join(this.args.config.path(exportPath), "postgres.tar.gz"));
}
else {
throw new error_1.FirebaseError("The Data Connect emulator is currently connected to a separate Postgres instance. Export is not supported.");
}
}
static async generate(args) {
const commandInfo = await (0, downloadableEmulators_1.downloadIfNecessary)(types_1.Emulators.DATACONNECT);
const cmd = [
"--logtostderr",
"-v=2",
"generate",
`--config_dir=${args.configDir}`,
`--connector_id=${args.connectorId}`,
];
if (args.watch) {
cmd.push("--watch");
}
const env = await DataConnectEmulator.getEnv(args.account);
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env });
if ((0, downloadableEmulators_1.isIncomaptibleArchError)(res.error)) {
throw new error_1.FirebaseError(`Unknown system error when running the Data Connect toolkit. ` +
`You may be able to fix this by installing Rosetta: ` +
`softwareupdate --install-rosetta`);
}
logger_1.logger.info(res.stderr);
if (res.error) {
throw new error_1.FirebaseError(`Error starting up Data Connect generate: ${res.error.message}`, {
original: res.error,
});
}
if (res.status !== 0) {
throw new error_1.FirebaseError(`Unable to generate your Data Connect SDKs (exit code ${res.status}): ${res.stderr}`);
}
return res.stdout;
}
static async build(args) {
var _a;
const commandInfo = await (0, downloadableEmulators_1.downloadIfNecessary)(types_1.Emulators.DATACONNECT);
const cmd = ["--logtostderr", "-v=2", "build", `--config_dir=${args.configDir}`];
if (args.projectId) {
cmd.push(`--project_id=${args.projectId}`);
}
const env = await DataConnectEmulator.getEnv(args.account);
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env });
if ((0, downloadableEmulators_1.isIncomaptibleArchError)(res.error)) {
throw new error_1.FirebaseError(`Unkown system error when running the Data Connect toolkit. ` +
`You may be able to fix this by installing Rosetta: ` +
`softwareupdate --install-rosetta`);
}
if (res.error) {
throw new error_1.FirebaseError(`Error starting up Data Connect build: ${res.error.message}`, {
original: res.error,
});
}
if (res.status !== 0) {
throw new error_1.FirebaseError(`Unable to build your Data Connect schema and connectors (exit code ${res.status}): ${res.stderr}`);
}
if (res.stderr) {
emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.DATACONNECT).log("DEBUG", res.stderr);
}
try {
return JSON.parse(res.stdout);
}
catch (err) {
throw new error_1.FirebaseError(`Unable to parse 'fdc build' output: ${(_a = res.stdout) !== null && _a !== void 0 ? _a : res.stderr}`);
}
}
async connectToPostgres(connectionString, database, serviceId) {
if (!connectionString) {
const msg = `No Postgres connection found. The Data Connect emulator will not be able to execute operations.`;
throw new error_1.FirebaseError(msg);
}
const MAX_RETRIES = 3;
for (let i = 1; i <= MAX_RETRIES; i++) {
try {
this.logger.logLabeled("DEBUG", "Data Connect", `Connecting to ${connectionString}}...`);
connectionString.toString();
await this.emulatorClient.configureEmulator({
connectionString: connectionString.toString(),
database,
serviceId,
maxOpenConnections: 1,
});
this.logger.logLabeled("DEBUG", "Data Connect", `Successfully connected to ${connectionString}}`);
return true;
}
catch (err) {
if (i === MAX_RETRIES) {
throw err;
}
this.logger.logLabeled("DEBUG", "Data Connect", `Retrying connectToPostgress call (${i} of ${MAX_RETRIES} attempts): ${err}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
return false;
}
static async getEnv(account, extraEnv = {}) {
const credsEnv = {};
if (account) {
const defaultCredPath = await (0, defaultCredentials_1.getCredentialPathAsync)(account);
if (defaultCredPath) {
logger_1.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
credsEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
}
}
return Object.assign(Object.assign(Object.assign({}, process.env), extraEnv), credsEnv);
}
}
exports.DataConnectEmulator = DataConnectEmulator;
class DataConnectEmulatorClient {
constructor() {
this.client = undefined;
}
async configureEmulator(body) {
var _a, _b;
if (!this.client) {
this.client = registry_1.EmulatorRegistry.client(types_1.Emulators.DATACONNECT);
}
try {
const res = await this.client.post("emulator/configure", body);
return res;
}
catch (err) {
if (err.status === 500) {
throw new error_1.FirebaseError(`Data Connect emulator: ${(_b = (_a = err === null || err === void 0 ? void 0 : err.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.message}`);
}
throw err;
}
}
async getInfo() {
if (!this.client) {
this.client = registry_1.EmulatorRegistry.client(types_1.Emulators.DATACONNECT);
}
return getInfo(this.client);
}
}
exports.DataConnectEmulatorClient = DataConnectEmulatorClient;
async function getInfo(client) {
try {
const res = await client.get("emulator/info");
return res.body;
}
catch (err) {
return;
}
}