Skip to content
251 changes: 140 additions & 111 deletions src/init/features/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,49 @@ async function askQuestions(setup: Setup): Promise<RequiredInfo> {
shouldProvisionCSQL: false,
};
const isBillingEnabled = setup.projectId ? await checkBillingEnabled(setup.projectId) : false;
info = await promptForService(setup, info, isBillingEnabled);
if (setup.projectId) {
isBillingEnabled ? await ensureApis(setup.projectId) : await ensureSparkApis(setup.projectId);
}
info = await checkExistingInstances(setup, info, isBillingEnabled);

if (info.cloudSqlInstanceId === "") {
const requiredConfigUnset =
info.serviceId === "" ||
info.cloudSqlInstanceId === "" ||
info.locationId === "" ||
info.cloudSqlDatabase === "";
const shouldConfigureBackend =
isBillingEnabled && requiredConfigUnset
? await confirm({
message: `Would you like to configure your backend resources now?`,
default: false,
})
: false;
if (shouldConfigureBackend) {
info = await promptForService(info);
info = await promptForCloudSQLInstance(setup, info);
}
info = await promptForDatabase(info);

if (info.cloudSqlDatabase === "") {
info = await promptForDatabase(setup, info);
info.shouldProvisionCSQL = !!(
setup.projectId &&
(info.isNewInstance || info.isNewDatabase) &&
isBillingEnabled &&
(await confirm({
message: `Would you like to provision your Cloud SQL instance and database now?${info.isNewInstance ? " This will take several minutes." : ""}.`,
default: true,
}))
);
} else {
if (requiredConfigUnset) {
logBullet(
`Setting placeholder values in dataconnect.yaml. You can edit these before you deploy to specify different IDs or regions.`,
);
}
info.serviceId = info.serviceId !== "" ? info.serviceId : basename(process.cwd());
info.cloudSqlInstanceId =
info.cloudSqlInstanceId !== "" ? info.cloudSqlInstanceId : `${info.serviceId || "app"}-fdc`;
info.locationId = info.locationId !== "" ? info.locationId : `us-central1`;
info.cloudSqlDatabase = info.cloudSqlDatabase !== "" ? info.cloudSqlDatabase : `fdcdb`;
}

info.shouldProvisionCSQL = !!(
setup.projectId &&
(info.isNewInstance || info.isNewDatabase) &&
isBillingEnabled &&
(await confirm({
message: `Would you like to provision your Cloud SQL instance and database now?${info.isNewInstance ? " This will take several minutes." : ""}.`,
default: true,
}))
);
return info;
}

Expand Down Expand Up @@ -222,90 +246,78 @@ function subConnectorYamlValues(replacementValues: { connectorId: string }): str
return replaced;
}

async function promptForService(
async function checkExistingInstances(
setup: Setup,
info: RequiredInfo,
isBillingEnabled: boolean,
): Promise<RequiredInfo> {
if (setup.projectId) {
if (isBillingEnabled) {
// Enabling compute.googleapis.com requires a Blaze plan.
await ensureApis(setup.projectId);
// TODO (b/344021748): Support initing with services that have existing sources/files
const existingServices = await listAllServices(setup.projectId);
const existingServicesAndSchemas = await Promise.all(
existingServices.map(async (s) => {
return {
service: s,
schema: await getSchema(s.name),
};
}),
);
if (existingServicesAndSchemas.length) {
const choices: { name: string; value: any }[] = existingServicesAndSchemas.map((s) => {
const serviceName = parseServiceName(s.service.name);
return {
name: `${serviceName.location}/${serviceName.serviceId}`,
value: s,
};
});
choices.push({ name: "Create a new service", value: undefined });
const choice: { service: Service; schema: Schema } = await promptOnce({
message:
"Your project already has existing services. Which would you like to set up local files for?",
type: "list",
choices,
});
if (choice) {
const serviceName = parseServiceName(choice.service.name);
info.serviceId = serviceName.serviceId;
info.locationId = serviceName.location;
if (choice.schema) {
const primaryDatasource = choice.schema.datasources.find((d) => d.postgresql);
if (primaryDatasource?.postgresql?.cloudSql.instance) {
const instanceName = parseCloudSQLInstanceName(
primaryDatasource.postgresql.cloudSql.instance,
);
info.cloudSqlInstanceId = instanceName.instanceId;
}
if (choice.schema.source.files) {
info.schemaGql = choice.schema.source.files;
}
info.cloudSqlDatabase = primaryDatasource?.postgresql?.database ?? "";
const connectors = await 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 || [],
};
});
}
}
}
}
} else {
await ensureSparkApis(setup.projectId);
}
if (!setup.projectId || !isBillingEnabled) {
// TODO(b/368609569): Don't gate this behind billing once backend billing fix is rolled out.
return info;
}

if (info.serviceId === "") {
info.serviceId = await promptOnce({
message: "What ID would you like to use for this service?",
type: "input",
default: basename(process.cwd()),
// Check for existing Firebase Data Connect services.
const existingServices = await listAllServices(setup.projectId);
const existingServicesAndSchemas = await Promise.all(
existingServices.map(async (s) => {
return {
service: s,
schema: await getSchema(s.name),
};
}),
);
if (existingServicesAndSchemas.length) {
const choices: { name: string; value: { service: Service; schema?: Schema } | undefined }[] =
existingServicesAndSchemas.map((s) => {
const serviceName = parseServiceName(s.service.name);
return {
name: `${serviceName.location}/${serviceName.serviceId}`,
value: s,
};
});
choices.push({ name: "Create a new service", value: undefined });
const choice: { service: Service; schema?: Schema } | undefined = await promptOnce({
message:
"Your project already has existing services. Which would you like to set up local files for?",
type: "list",
choices,
});
if (choice) {
const serviceName = parseServiceName(choice.service.name);
info.serviceId = serviceName.serviceId;
info.locationId = serviceName.location;
if (choice.schema) {
const primaryDatasource = choice.schema.datasources.find((d) => d.postgresql);
if (primaryDatasource?.postgresql?.cloudSql.instance) {
const instanceName = parseCloudSQLInstanceName(
primaryDatasource.postgresql.cloudSql.instance,
);
info.cloudSqlInstanceId = instanceName.instanceId;
}
if (choice.schema.source.files) {
info.schemaGql = choice.schema.source.files;
}
info.cloudSqlDatabase = primaryDatasource?.postgresql?.database ?? "";
const connectors = await 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 promptForCloudSQLInstance(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
if (setup.projectId) {
// Check for existing Cloud SQL instances, if we didn't already set one.
if (info.cloudSqlInstanceId === "") {
const instances = await cloudsql.listInstances(setup.projectId);
let choices = instances.map((i) => {
return { name: i.name, value: i.name, location: i.region };
Expand All @@ -328,6 +340,43 @@ async function promptForCloudSQLInstance(setup: Setup, info: RequiredInfo): Prom
}
}
}

// Check for existing Cloud SQL databases, if we didn't already set one.
if (info.cloudSqlDatabase === "" && info.cloudSqlInstanceId !== "") {
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 promptOnce({
message: `Which database in ${info.cloudSqlInstanceId} would you like to use?`,
type: "list",
choices,
});
}
} catch (err) {
// Show existing databases in a list is optional, ignore any errors from ListDatabases.
// This often happen when the Cloud SQL instance is still being created.
logger.debug(`[dataconnect] Cannot list databases during init: ${err}`);
}
}
return info;
}

async function promptForService(info: RequiredInfo): Promise<RequiredInfo> {
if (info.serviceId === "") {
info.serviceId = await promptOnce({
message: "What ID would you like to use for this service?",
type: "input",
default: basename(process.cwd()),
});
}
return info;
}

async function promptForCloudSQLInstance(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
if (info.cloudSqlInstanceId === "") {
info.isNewInstance = true;
info.cloudSqlInstanceId = await promptOnce({
Expand Down Expand Up @@ -368,27 +417,7 @@ async function locationChoices(setup: Setup) {
}
}

async function promptForDatabase(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
if (!info.isNewInstance && 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 promptOnce({
message: `Which database in ${info.cloudSqlInstanceId} would you like to use?`,
type: "list",
choices,
});
}
} catch (err) {
// Show existing databases in a list is optional, ignore any errors from ListDatabases.
// This often happen when the Cloud SQL instance is still being created.
logger.debug(`[dataconnect] Cannot list databases during init: ${err}`);
}
}
async function promptForDatabase(info: RequiredInfo): Promise<RequiredInfo> {
if (info.cloudSqlDatabase === "") {
info.isNewDatabase = true;
info.cloudSqlDatabase = await promptOnce({
Expand Down