Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Add a confirmation in `firebase init dataconnect` before asking for app idea description. (#9282)
- Update dataconnect:\* commands to use flags for --service & --location (#9312)
19 changes: 14 additions & 5 deletions src/commands/dataconnect-sdk-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@ import { Command } from "../command";
import { Options } from "../options";
import { DataConnectEmulator } from "../emulator/dataconnectEmulator";
import { needProjectId } from "../projectUtils";
import { loadAll } from "../dataconnect/load";
import { pickServices } from "../dataconnect/load";
import { logger } from "../logger";
import { getProjectDefaultAccount } from "../auth";
import { logLabeledSuccess } from "../utils";
import { ServiceInfo } from "../dataconnect/types";

type GenerateOptions = Options & { watch?: boolean };
type GenerateOptions = Options & { watch?: boolean; service?: string; location?: string };

export const command = new Command("dataconnect:sdk:generate")
.description("generate typed SDKs for your Data Connect connectors")
.description("generate typed SDKs to use Data Connect in your apps")
.option(
"--service <serviceId>",
"the serviceId of the Data Connect service. If not provided, generates SDKs for all services.",
)
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.option(
"--watch",
"watch for changes to your connector GQL files and regenerate your SDKs when updates occur",
)
.action(async (options: GenerateOptions) => {
const projectId = needProjectId(options);

const serviceInfos = await loadAll(projectId, options.config);
const serviceInfos = await pickServices(
projectId,
options.config,
options.service,
options.location,
);
const serviceInfosWithSDKs = serviceInfos.filter((serviceInfo) =>
serviceInfo.connectorInfo.some((c) => {
return (
Expand Down
21 changes: 15 additions & 6 deletions src/commands/dataconnect-sql-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,38 @@ import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { ensureApis } from "../dataconnect/ensureApis";
import { requirePermissions } from "../requirePermissions";
import { pickService } from "../dataconnect/load";
import { pickOneService } from "../dataconnect/load";
import { diffSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";

export const command = new Command("dataconnect:sql:diff [serviceId]")
type DiffOptions = Options & { service?: string; location?: string };

export const command = new Command("dataconnect:sql:diff")
.description(
"display the differences between a local Data Connect schema and your CloudSQL database's current schema",
"display the differences between the local Data Connect schema and your CloudSQL database's schema",
)
.option("--service <serviceId>", "the serviceId of the Data Connect service")
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.before(requirePermissions, [
"firebasedataconnect.services.list",
"firebasedataconnect.schemas.list",
"firebasedataconnect.schemas.update",
])
.before(requireAuth)
.action(async (serviceId: string, options: Options) => {
.action(async (options: DiffOptions) => {
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const serviceInfo = await pickOneService(
projectId,
options.config,
options.service,
options.location,
);

const diffs = await diffSchema(
options,
serviceInfo.schema,
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
);
return { projectId, serviceId, diffs };
return { projectId, diffs };
});
40 changes: 26 additions & 14 deletions src/commands/dataconnect-sql-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { needProjectId } from "../projectUtils";
import { ensureApis } from "../dataconnect/ensureApis";
import { requirePermissions } from "../requirePermissions";
import { pickService } from "../dataconnect/load";
import { pickOneService } from "../dataconnect/load";
import { grantRoleToUserInSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
Expand All @@ -12,45 +12,57 @@

const allowedRoles = Object.keys(fdcSqlRoleMap);

export const command = new Command("dataconnect:sql:grant [serviceId]")
type GrantOptions = Options & {
role?: string;
email?: string;
service?: string;
location?: string;
};

export const command = new Command("dataconnect:sql:grant")
.description("grants the SQL role <role> to the provided user or service account <email>")
.option("-R, --role <role>", "The SQL role to grant. One of: owner, writer, or reader.")
.option(
"-E, --email <email>",
"The email of the user or service account we would like to grant the role to.",
)
.option("--service <serviceId>", "the serviceId of the Data Connect service")
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.before(requirePermissions, ["firebasedataconnect.services.list"])
.before(requireAuth)
.action(async (serviceId: string, options: Options) => {
const role = options.role as string;
const email = options.email as string;
if (!role) {
.action(async (options: GrantOptions) => {
if (!options.role) {
throw new FirebaseError(
"-R, --role <role> is required. Run the command with -h for more info.",
);
}
if (!email) {
if (!options.email) {
throw new FirebaseError(
"-E, --email <email> is required. Run the command with -h for more info.",
);
}

if (!allowedRoles.includes(role.toLowerCase())) {
if (!allowedRoles.includes(options.role.toLowerCase())) {
throw new FirebaseError(`Role should be one of ${allowedRoles.join(" | ")}.`);
}

const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickOneService(
projectId,
options.config,
options.service as string | undefined,

Check warning on line 54 in src/commands/dataconnect-sql-grant.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression
options.location as string | undefined,

Check warning on line 55 in src/commands/dataconnect-sql-grant.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression
);

// Make sure current user can perform this action.
const userIsCSQLAdmin = await iamUserIsCSQLAdmin(options);
if (!userIsCSQLAdmin) {
throw new FirebaseError(
`Only users with 'roles/cloudsql.admin' can grant SQL roles. If you do not have this role, ask your database administrator to run this command or manually grant ${role} to ${email}`,
`Only users with 'roles/cloudsql.admin' can grant SQL roles. If you do not have this role, ask your database administrator to run this command or manually grant ${options.role} to ${options.email}`,
);
}

const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);

await grantRoleToUserInSchema(options, serviceInfo.schema);
return { projectId, serviceId };
return { projectId };
});
19 changes: 14 additions & 5 deletions src/commands/dataconnect-sql-migrate.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { pickService } from "../dataconnect/load";
import { pickOneService } from "../dataconnect/load";
import { FirebaseError } from "../error";
import { migrateSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { requirePermissions } from "../requirePermissions";
import { ensureApis } from "../dataconnect/ensureApis";
import { logLabeledSuccess } from "../utils";

export const command = new Command("dataconnect:sql:migrate [serviceId]")
type MigrateOptions = Options & { service?: string; location?: string };

export const command = new Command("dataconnect:sql:migrate")
.description("migrate your CloudSQL database's schema to match your local Data Connect schema")
.option("--service <serviceId>", "the serviceId of the Data Connect service")
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.before(requirePermissions, [
"firebasedataconnect.services.list",
"firebasedataconnect.schemas.list",
Expand All @@ -19,10 +23,15 @@ export const command = new Command("dataconnect:sql:migrate [serviceId]")
])
.before(requireAuth)
.withForce("execute any required database changes without prompting")
.action(async (serviceId: string, options: Options) => {
.action(async (options: MigrateOptions) => {
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const serviceInfo = await pickOneService(
projectId,
options.config,
options.service,
options.location,
);
const instanceId =
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
if (!instanceId) {
Expand All @@ -44,5 +53,5 @@ export const command = new Command("dataconnect:sql:migrate [serviceId]")
} else {
logLabeledSuccess("dataconnect", "Database schema is already up to date!");
}
return { projectId, serviceId, diffs };
return { projectId, diffs };
});
17 changes: 13 additions & 4 deletions src/commands/dataconnect-sql-setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { pickService } from "../dataconnect/load";
import { FirebaseError } from "../error";
import { requireAuth } from "../requireAuth";
import { requirePermissions } from "../requirePermissions";
Expand All @@ -10,20 +9,30 @@ import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissi
import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions";
import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconnect/schemaMigration";
import { setupIAMUsers } from "../gcp/cloudsql/connect";
import { pickOneService } from "../dataconnect/load";

export const command = new Command("dataconnect:sql:setup [serviceId]")
type SetupOptions = Options & { service?: string; location?: string };

export const command = new Command("dataconnect:sql:setup")
.description("set up your CloudSQL database")
.option("--service <serviceId>", "the serviceId of the Data Connect service")
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.before(requirePermissions, [
"firebasedataconnect.services.list",
"firebasedataconnect.schemas.list",
"firebasedataconnect.schemas.update",
"cloudsql.instances.connect",
])
.before(requireAuth)
.action(async (serviceId: string, options: Options) => {
.action(async (options: SetupOptions) => {
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const serviceInfo = await pickOneService(
projectId,
options.config,
options.service,
options.location,
);
const instanceId =
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
if (!instanceId) {
Expand Down
19 changes: 14 additions & 5 deletions src/commands/dataconnect-sql-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { needProjectId } from "../projectUtils";
import { ensureApis } from "../dataconnect/ensureApis";
import { requirePermissions } from "../requirePermissions";
import { pickService } from "../dataconnect/load";
import { pickOneService } from "../dataconnect/load";
import { getIdentifiers } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { getIAMUser } from "../gcp/cloudsql/connect";
Expand Down Expand Up @@ -62,8 +62,8 @@
return query;
}

async function mainShellLoop(conn: pg.PoolClient) {

Check warning on line 65 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
while (true) {

Check warning on line 66 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected constant condition
const query = await promptForQuery();
if (query.toLowerCase() === ".exit") {
break;
Expand All @@ -81,16 +81,25 @@
}
}

export const command = new Command("dataconnect:sql:shell [serviceId]")
type ShellOptions = Options & { service?: string; location?: string };

export const command = new Command("dataconnect:sql:shell")
.description(
"start a shell connected directly to your Data Connect service's linked CloudSQL instance",
)
.option("--service <serviceId>", "the serviceId of the Data Connect service")
.option("--location <location>", "the location of the Data Connect service to disambiguate")
.before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"])
.before(requireAuth)
.action(async (serviceId: string, options: Options) => {
.action(async (options: ShellOptions) => {
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const serviceInfo = await pickOneService(
projectId,
options.config,
options.service,
options.location,
);
const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema);
const { user: username } = await getIAMUser(options);
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
Expand All @@ -99,7 +108,7 @@
const connectionName = instance.connectionName;
if (!connectionName) {
throw new FirebaseError(
`Could not get instance connection string for ${options.instanceId}:${options.databaseId}`,

Check warning on line 111 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression

Check warning on line 111 in src/commands/dataconnect-sql-shell.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
);
}
const connector: Connector = new Connector({
Expand Down Expand Up @@ -134,5 +143,5 @@
await pool.end();
connector.close();

return { projectId, serviceId };
return { projectId };
});
68 changes: 40 additions & 28 deletions src/dataconnect/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,56 @@
import { readFileFromDirectory, wrappedSafeLoad } from "../utils";
import { DataConnectMultiple } from "../firebaseConfig";

// pickService reads firebase.json and returns all services with a given serviceId.
// If serviceID is not provided and there is a single service, return that.
export async function pickService(
/** Picks exactly one Data Connect service based on flags. */
export async function pickOneService(
projectId: string,
config: Config,
serviceId?: string,
service?: string,
location?: string,
): Promise<ServiceInfo> {
const services = await pickServices(projectId, config, service, location);
if (services.length > 1) {
const serviceIds = services.map(
(i) => `${i.dataConnectYaml.location}:${i.dataConnectYaml.serviceId}`,
);
throw new FirebaseError(
`Multiple services matched. Please specify a service and location. Matched services: ${serviceIds.join(
", ",
)}`,
);
}
return services[0];
}

/** Picks Data Connect services based on flags. */
export async function pickServices(
projectId: string,
config: Config,
serviceId?: string,
location?: string,
): Promise<ServiceInfo[]> {
const serviceInfos = await loadAll(projectId, config);
if (serviceInfos.length === 0) {
throw new FirebaseError(
"No Data Connect services found in firebase.json." +
`\nYou can run ${clc.bold("firebase init dataconnect")} to add a Data Connect service.`,
);
} else if (serviceInfos.length === 1) {
if (serviceId && serviceId !== serviceInfos[0].dataConnectYaml.serviceId) {
throw new FirebaseError(
`No service named ${serviceId} declared in firebase.json. Found ${serviceInfos[0].dataConnectYaml.serviceId}.` +
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
);
}
return serviceInfos[0];
} else {
if (!serviceId) {
throw new FirebaseError(
"Multiple Data Connect services found in firebase.json. Please specify a service ID to use.",
);
}
// TODO: handle cases where there are services with the same ID in 2 locations.
const maybe = serviceInfos.find((i) => i.dataConnectYaml.serviceId === serviceId);
if (!maybe) {
const serviceIds = serviceInfos.map((i) => i.dataConnectYaml.serviceId);
throw new FirebaseError(
`No service named ${serviceId} declared in firebase.json. Found ${serviceIds.join(", ")}.` +
`\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`,
);
}
return maybe;
}

const matchingServices = serviceInfos.filter(
(i) =>
(!serviceId || i.dataConnectYaml.serviceId === serviceId) &&
(!location || i.dataConnectYaml.location === location),
);
if (matchingServices.length === 0) {
const serviceIds = serviceInfos.map(
(i) => `${i.dataConnectYaml.location}:${i.dataConnectYaml.serviceId}`,
);
throw new FirebaseError(
`No service matched service in firebase.json. Available services: ${serviceIds.join(", ")}`,
);
}
return matchingServices;
}

/**
Expand Down Expand Up @@ -112,12 +124,12 @@
};
}

export function readFirebaseJson(config?: Config): DataConnectMultiple {

Check warning on line 127 in src/dataconnect/load.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
if (!config?.has("dataconnect")) {
return [];
}
const validator = (cfg: any) => {

Check warning on line 131 in src/dataconnect/load.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 131 in src/dataconnect/load.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (!cfg["source"]) {

Check warning on line 132 in src/dataconnect/load.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access ["source"] on an `any` value
throw new FirebaseError("Invalid firebase.json: DataConnect requires `source`");
}
return {
Expand Down
Loading
Loading