firebase-tools
Version:
Command-Line Interface for Firebase
368 lines (367 loc) • 16.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.toDNSCompatibleId = exports.postSetup = exports.actuate = exports.askQuestions = void 0;
const path_1 = require("path");
const clc = require("colorette");
const fs = require("fs-extra");
const prompt_1 = require("../../../prompt");
const provisionCloudSql_1 = require("../../../dataconnect/provisionCloudSql");
const freeTrial_1 = require("../../../dataconnect/freeTrial");
const cloudsql = require("../../../gcp/cloudsql/cloudsqladmin");
const ensureApis_1 = require("../../../dataconnect/ensureApis");
const client_1 = require("../../../dataconnect/client");
const types_1 = require("../../../dataconnect/types");
const names_1 = require("../../../dataconnect/names");
const logger_1 = require("../../../logger");
const templates_1 = require("../../../templates");
const utils_1 = require("../../../utils");
const cloudbilling_1 = require("../../../gcp/cloudbilling");
const sdk = require("./sdk");
const fileUtils_1 = require("../../../dataconnect/fileUtils");
const DATACONNECT_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/dataconnect.yaml");
const CONNECTOR_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/connector.yaml");
const SCHEMA_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/schema.gql");
const QUERIES_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/queries.gql");
const MUTATIONS_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/mutations.gql");
const emptyConnector = {
id: "default",
path: "./connector",
files: [],
};
const defaultConnector = {
id: "default",
path: "./connector",
files: [
{
path: "queries.gql",
content: QUERIES_TEMPLATE,
},
{
path: "mutations.gql",
content: MUTATIONS_TEMPLATE,
},
],
};
const defaultSchema = { path: "schema.gql", content: SCHEMA_TEMPLATE };
async function askQuestions(setup) {
const hasBilling = await (0, cloudbilling_1.isBillingEnabled)(setup);
if (setup.projectId) {
hasBilling ? await (0, ensureApis_1.ensureApis)(setup.projectId) : await (0, ensureApis_1.ensureSparkApis)(setup.projectId);
}
let info = {
serviceId: "",
locationId: "",
cloudSqlInstanceId: "",
isNewInstance: false,
cloudSqlDatabase: "",
isNewDatabase: false,
connectors: [],
schemaGql: [],
shouldProvisionCSQL: false,
};
info = await promptForExistingServices(setup, info);
const requiredConfigUnset = info.serviceId === "" ||
info.cloudSqlInstanceId === "" ||
info.locationId === "" ||
info.cloudSqlDatabase === "";
const shouldConfigureBackend = hasBilling &&
requiredConfigUnset &&
(await (0, prompt_1.confirm)({
message: `Would you like to configure your Cloud SQL datasource now?`,
default: true,
}));
if (shouldConfigureBackend) {
info = await promptForService(info);
info = await promptForCloudSQL(setup, info);
info.shouldProvisionCSQL = !!(setup.projectId &&
(info.isNewInstance || info.isNewDatabase) &&
hasBilling &&
(await (0, prompt_1.confirm)({
message: `Would you like to provision your Cloud SQL instance and database now?${info.isNewInstance ? " This will take several minutes." : ""}.`,
default: true,
})));
}
setup.featureInfo = setup.featureInfo || {};
setup.featureInfo.dataconnect = info;
}
exports.askQuestions = askQuestions;
async function actuate(setup, config, options) {
var _a;
const dir = config.get("dataconnect.source", "dataconnect");
const dataDir = config.get("emulators.dataconnect.dataDir", `${dir}/.dataconnect/pgliteData`);
config.set("emulators.dataconnect.dataDir", dataDir);
const info = (_a = setup.featureInfo) === null || _a === void 0 ? void 0 : _a.dataconnect;
if (!info) {
throw new Error("Data Connect feature RequiredInfo is not provided");
}
const defaultServiceId = toDNSCompatibleId((0, path_1.basename)(process.cwd()));
info.serviceId = info.serviceId || defaultServiceId;
info.cloudSqlInstanceId =
info.cloudSqlInstanceId || `${info.serviceId.toLowerCase() || "app"}-fdc`;
info.locationId = info.locationId || `us-central1`;
info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;
if (!info.schemaGql.length && !info.connectors.flatMap((r) => r.files).length) {
info.schemaGql = [defaultSchema];
info.connectors = [defaultConnector];
}
await writeFiles(config, info, options);
if (setup.projectId && info.shouldProvisionCSQL) {
await (0, provisionCloudSql_1.provisionCloudSql)({
projectId: setup.projectId,
location: info.locationId,
instanceId: info.cloudSqlInstanceId,
databaseId: info.cloudSqlDatabase,
enableGoogleMlIntegration: false,
waitForCreation: false,
});
}
}
exports.actuate = actuate;
async function postSetup(setup, config) {
const cwdPlatformGuess = await (0, fileUtils_1.getPlatformFromFolder)(process.cwd());
if (cwdPlatformGuess !== types_1.Platform.NONE || (0, utils_1.envOverride)("FDC_CONNECTOR", "")) {
await sdk.doSetup(setup, config);
}
else {
(0, utils_1.logBullet)(`If you'd like to add the generated SDK to your app later, run ${clc.bold("firebase init dataconnect:sdk")}`);
}
if (setup.projectId && !setup.isBillingEnabled) {
(0, utils_1.logBullet)((0, freeTrial_1.upgradeInstructions)(setup.projectId));
}
}
exports.postSetup = postSetup;
async function writeFiles(config, info, options) {
const dir = config.get("dataconnect.source") || "dataconnect";
const subbedDataconnectYaml = subDataconnectYamlValues(Object.assign(Object.assign({}, info), { connectorDirs: info.connectors.map((c) => c.path) }));
config.set("dataconnect", { source: dir });
await config.askWriteProjectFile((0, path_1.join)(dir, "dataconnect.yaml"), subbedDataconnectYaml, !!options.force, true);
if (info.schemaGql.length) {
for (const f of info.schemaGql) {
await config.askWriteProjectFile((0, path_1.join)(dir, "schema", f.path), f.content, !!options.force);
}
}
else {
fs.ensureFileSync((0, path_1.join)(dir, "schema", "schema.gql"));
}
for (const c of info.connectors) {
await writeConnectorFiles(config, c);
}
}
async function writeConnectorFiles(config, connectorInfo) {
const subbedConnectorYaml = subConnectorYamlValues({ connectorId: connectorInfo.id });
const dir = config.get("dataconnect.source") || "dataconnect";
await config.askWriteProjectFile((0, path_1.join)(dir, connectorInfo.path, "connector.yaml"), subbedConnectorYaml);
for (const f of connectorInfo.files) {
await config.askWriteProjectFile((0, path_1.join)(dir, connectorInfo.path, f.path), f.content);
}
}
function subDataconnectYamlValues(replacementValues) {
const replacements = {
serviceId: "__serviceId__",
cloudSqlDatabase: "__cloudSqlDatabase__",
cloudSqlInstanceId: "__cloudSqlInstanceId__",
connectorDirs: "__connectorDirs__",
locationId: "__location__",
};
let replaced = DATACONNECT_YAML_TEMPLATE;
for (const [k, v] of Object.entries(replacementValues)) {
replaced = replaced.replace(replacements[k], JSON.stringify(v));
}
return replaced;
}
function subConnectorYamlValues(replacementValues) {
const replacements = {
connectorId: "__connectorId__",
};
let replaced = CONNECTOR_YAML_TEMPLATE;
for (const [k, v] of Object.entries(replacementValues)) {
replaced = replaced.replace(replacements[k], JSON.stringify(v));
}
return replaced;
}
async function promptForExistingServices(setup, info) {
var _a, _b, _c, _d;
if (!setup.projectId) {
return info;
}
const existingServices = await (0, client_1.listAllServices)(setup.projectId);
const existingServicesAndSchemas = await Promise.all(existingServices.map(async (s) => {
return { service: s, schema: await (0, client_1.getSchema)(s.name) };
}));
if (existingServicesAndSchemas.length) {
const choice = await chooseExistingService(existingServicesAndSchemas);
if (choice) {
const serviceName = (0, names_1.parseServiceName)(choice.service.name);
info.serviceId = serviceName.serviceId;
info.locationId = serviceName.location;
info.schemaGql = [];
info.connectors = [emptyConnector];
if (choice.schema) {
const primaryDatasource = choice.schema.datasources.find((d) => d.postgresql);
if ((_a = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instance) {
const instanceName = (0, names_1.parseCloudSQLInstanceName)(primaryDatasource.postgresql.cloudSql.instance);
info.cloudSqlInstanceId = instanceName.instanceId;
}
if ((_b = choice.schema.source.files) === null || _b === void 0 ? void 0 : _b.length) {
info.schemaGql = choice.schema.source.files;
}
info.cloudSqlDatabase = (_d = (_c = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _c === void 0 ? void 0 : _c.database) !== null && _d !== void 0 ? _d : "";
const connectors = await (0, client_1.listConnectors)(choice.service.name, [
"connectors.name",
"connectors.source.files",
]);
if (connectors.length) {
info.connectors = connectors.map((c) => {
const id = c.name.split("/").pop();
return {
id,
path: connectors.length === 1 ? "./connector" : `./${id}`,
files: c.source.files || [],
};
});
}
}
}
}
return info;
}
async function chooseExistingService(existing) {
const serviceEnvVar = (0, utils_1.envOverride)("FDC_CONNECTOR", "") || (0, utils_1.envOverride)("FDC_SERVICE", "");
if (serviceEnvVar) {
const [serviceLocationFromEnvVar, serviceIdFromEnvVar] = serviceEnvVar.split("/");
const serviceFromEnvVar = existing.find((s) => {
const serviceName = (0, names_1.parseServiceName)(s.service.name);
return (serviceName.serviceId === serviceIdFromEnvVar &&
serviceName.location === serviceLocationFromEnvVar);
});
if (serviceFromEnvVar) {
(0, utils_1.logBullet)(`Picking up the existing service ${clc.bold(serviceLocationFromEnvVar + "/" + serviceIdFromEnvVar)}.`);
return serviceFromEnvVar;
}
(0, utils_1.logWarning)(`Unable to pick up an existing service based on FDC_SERVICE=${serviceEnvVar}.`);
}
const choices = existing.map((s) => {
const serviceName = (0, names_1.parseServiceName)(s.service.name);
return {
name: `${serviceName.location}/${serviceName.serviceId}`,
value: s,
};
});
choices.push({ name: "Create a new service", value: undefined });
return await (0, prompt_1.select)({
message: "Your project already has existing services. Which would you like to set up local files for?",
choices,
});
}
async function promptForCloudSQL(setup, info) {
if (info.cloudSqlInstanceId === "" && setup.projectId) {
const instances = await cloudsql.listInstances(setup.projectId);
let choices = instances.map((i) => {
var _a;
let display = `${i.name} (${i.region})`;
if (((_a = i.settings.userLabels) === null || _a === void 0 ? void 0 : _a["firebase-data-connect"]) === "ft") {
display += " (no cost trial)";
}
return { name: display, value: i.name, location: i.region };
});
choices = choices.filter((c) => info.locationId === "" || info.locationId === c.location);
if (choices.length) {
if (!(await (0, freeTrial_1.checkFreeTrialInstanceUsed)(setup.projectId))) {
choices.push({ name: "Create a new free trial instance", value: "", location: "" });
}
else {
choices.push({ name: "Create a new CloudSQL instance", value: "", location: "" });
}
info.cloudSqlInstanceId = await (0, prompt_1.select)({
message: `Which CloudSQL instance would you like to use?`,
choices,
});
if (info.cloudSqlInstanceId !== "") {
info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId).location;
}
}
}
if (info.cloudSqlInstanceId === "") {
info.isNewInstance = true;
info.cloudSqlInstanceId = await (0, prompt_1.input)({
message: `What ID would you like to use for your new CloudSQL instance?`,
default: `${info.serviceId.toLowerCase() || "app"}-fdc`,
});
}
if (info.locationId === "") {
const choices = await locationChoices(setup);
info.locationId = await (0, prompt_1.select)({
message: "What location would like to use?",
choices,
});
}
if (info.cloudSqlDatabase === "" && setup.projectId) {
try {
const dbs = await cloudsql.listDatabases(setup.projectId, info.cloudSqlInstanceId);
const choices = dbs.map((d) => {
return { name: d.name, value: d.name };
});
choices.push({ name: "Create a new database", value: "" });
if (dbs.length) {
info.cloudSqlDatabase = await (0, prompt_1.select)({
message: `Which database in ${info.cloudSqlInstanceId} would you like to use?`,
choices,
});
}
}
catch (err) {
logger_1.logger.debug(`[dataconnect] Cannot list databases during init: ${err}`);
}
}
if (info.cloudSqlDatabase === "") {
info.isNewDatabase = true;
info.cloudSqlDatabase = await (0, prompt_1.input)({
message: `What ID would you like to use for your new database in ${info.cloudSqlInstanceId}?`,
default: `fdcdb`,
});
}
return info;
}
async function promptForService(info) {
if (info.serviceId === "") {
info.serviceId = await (0, prompt_1.input)({
message: "What ID would you like to use for this service?",
default: (0, path_1.basename)(process.cwd()),
});
}
return info;
}
async function locationChoices(setup) {
if (setup.projectId) {
const locations = await (0, client_1.listLocations)(setup.projectId);
return locations.map((l) => {
return { name: l, value: l };
});
}
else {
return [
{ name: "us-central1", value: "us-central1" },
{ name: "europe-north1", value: "europe-north1" },
{ name: "europe-central2", value: "europe-central2" },
{ name: "europe-west1", value: "europe-west1" },
{ name: "southamerica-west1", value: "southamerica-west1" },
{ name: "us-east4", value: "us-east4" },
{ name: "us-west1", value: "us-west1" },
{ name: "asia-southeast1", value: "asia-southeast1" },
];
}
}
function toDNSCompatibleId(id) {
let defaultServiceId = (0, path_1.basename)(id)
.toLowerCase()
.replaceAll(/[^a-z0-9-]/g, "")
.slice(0, 63);
while (defaultServiceId.endsWith("-") && defaultServiceId.length) {
defaultServiceId = defaultServiceId.slice(0, defaultServiceId.length - 1);
}
while (defaultServiceId.startsWith("-") && defaultServiceId.length) {
defaultServiceId = defaultServiceId.slice(1, defaultServiceId.length);
}
return defaultServiceId || "app";
}
exports.toDNSCompatibleId = toDNSCompatibleId;
;