From 9604c4b15d27d5a0455833b18e8da0ee22757694 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Mon, 13 Oct 2025 14:16:41 -0400 Subject: [PATCH 01/10] Remove overrides that diverge the test configuration from the build configuration. (#9300) Co-authored-by: Jamie Rothfeder --- tsconfig.compile.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tsconfig.compile.json b/tsconfig.compile.json index caedf05e4af..ae163696ed2 100644 --- a/tsconfig.compile.json +++ b/tsconfig.compile.json @@ -2,11 +2,6 @@ "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true, // Don't actually produce any compiled output. - "checkJs": true, // We want to do a pass on JS files. "resolveJsonModule": true, // Makes require(package.json) acceptable (added). - "strict": false, // Need to relax some strictness to check JS (changed). - "strictBindCallApply": true, // This is part of "strict: true" (added). - "strictFunctionTypes": true, // This is part of "strict: true" (added). - "noImplicitThis": true, // This is part of "strict: true" (added). } } From 115773193c90e0d1b3fd557950c439418f8a18d1 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 13 Oct 2025 13:35:39 -0700 Subject: [PATCH 02/10] Fix misleading typing for options.json. (#9275) --- firebase-vscode/src/options.ts | 1 - src/checkValidTargetFilters.spec.ts | 1 - src/command.ts | 1 + src/commands/firestore-backups-delete.ts | 6 +---- src/commands/firestore-backups-get.ts | 10 ++----- src/commands/firestore-backups-list.ts | 19 ++++++-------- .../firestore-backups-schedules-create.ts | 10 +++---- .../firestore-backups-schedules-delete.ts | 6 +---- .../firestore-backups-schedules-list.ts | 7 +---- .../firestore-backups-schedules-update.ts | 10 +++---- src/commands/firestore-bulkdelete.ts | 25 +++++++----------- src/commands/firestore-databases-create.ts | 22 +++++++--------- src/commands/firestore-databases-delete.ts | 6 +---- src/commands/firestore-databases-get.ts | 7 +---- src/commands/firestore-databases-list.ts | 7 +---- src/commands/firestore-databases-restore.ts | 26 ++++++++----------- src/commands/firestore-databases-update.ts | 6 +---- src/commands/firestore-locations.ts | 7 +---- .../firestore-operations-cancel.spec.ts | 9 ++----- src/commands/firestore-operations-cancel.ts | 11 +++----- .../firestore-operations-describe.spec.ts | 5 ++-- src/commands/firestore-operations-describe.ts | 9 ++----- .../firestore-operations-list.spec.ts | 6 ++--- src/commands/firestore-operations-list.ts | 9 ++----- src/deploy/apphosting/deploy.spec.ts | 1 - src/deploy/apphosting/prepare.spec.ts | 1 - src/deploy/apphosting/release.spec.ts | 1 - src/deploy/functions/prompts.spec.ts | 1 - src/deploy/hosting/prepare.spec.ts | 1 - src/emulator/extensions/validation.spec.ts | 1 - src/emulator/storage/rules/config.spec.ts | 1 - src/filterTargets.spec.ts | 1 - src/gcp/cloudsql/cloudsqladmin.spec.ts | 1 - src/init/features/functions.spec.ts | 1 - src/init/features/genkit/index.spec.ts | 1 - src/management/studio.spec.ts | 1 - src/options.ts | 15 ++++++++++- src/requireConfig.spec.ts | 1 - src/requireTosAcceptance.spec.ts | 1 - 39 files changed, 82 insertions(+), 173 deletions(-) diff --git a/firebase-vscode/src/options.ts b/firebase-vscode/src/options.ts index a829e6c9551..dc10941d725 100644 --- a/firebase-vscode/src/options.ts +++ b/firebase-vscode/src/options.ts @@ -26,7 +26,6 @@ const defaultOptions: Readonly = { projectNumber: "", projectRoot: "", account: "", - json: true, nonInteractive: true, interactive: false, debug: false, diff --git a/src/checkValidTargetFilters.spec.ts b/src/checkValidTargetFilters.spec.ts index e77100fe7c3..fc052498964 100644 --- a/src/checkValidTargetFilters.spec.ts +++ b/src/checkValidTargetFilters.spec.ts @@ -13,7 +13,6 @@ const SAMPLE_OPTIONS: Options = { only: "", except: "", nonInteractive: false, - json: false, interactive: false, debug: false, force: false, diff --git a/src/command.ts b/src/command.ts index 36d6ab502f6..1f82d68343a 100644 --- a/src/command.ts +++ b/src/command.ts @@ -313,6 +313,7 @@ export class Command { } if (getInheritedOption(options, "json")) { + options.interactive = false; options.nonInteractive = true; } else if (!options.isMCP) { useConsoleLoggers(); diff --git a/src/commands/firestore-backups-delete.ts b/src/commands/firestore-backups-delete.ts index 4fb8339f916..ecc6e8d538c 100644 --- a/src/commands/firestore-backups-delete.ts +++ b/src/commands/firestore-backups-delete.ts @@ -34,11 +34,7 @@ export const command = new Command("firestore:backups:delete ") throw new FirebaseError(`Failed to delete the backup ${backupName}`, { original: err }); } - if (options.json) { - logger.info(JSON.stringify(backup, undefined, 2)); - } else { - logger.info(clc.bold(`Successfully deleted ${clc.yellow(backupName)}`)); - } + logger.info(clc.bold(`Successfully deleted ${clc.yellow(backupName)}`)); return backup; }); diff --git a/src/commands/firestore-backups-get.ts b/src/commands/firestore-backups-get.ts index 73aaa677e30..3a030370590 100644 --- a/src/commands/firestore-backups-get.ts +++ b/src/commands/firestore-backups-get.ts @@ -1,9 +1,7 @@ import { Command } from "../command"; -import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -import { FirestoreOptions } from "../firestore/options"; import { Backup, getBackup } from "../gcp/firestore"; import { PrettyPrint } from "../firestore/pretty-print"; @@ -11,15 +9,11 @@ export const command = new Command("firestore:backups:get ") .description("get a Cloud Firestore database backup") .before(requirePermissions, ["datastore.backups.get"]) .before(warnEmulatorNotSupported, Emulators.FIRESTORE) - .action(async (backupName: string, options: FirestoreOptions) => { + .action(async (backupName: string) => { const backup: Backup = await getBackup(backupName); const printer = new PrettyPrint(); - if (options.json) { - logger.info(JSON.stringify(backup, undefined, 2)); - } else { - printer.prettyPrintBackup(backup); - } + printer.prettyPrintBackup(backup); return backup; }); diff --git a/src/commands/firestore-backups-list.ts b/src/commands/firestore-backups-list.ts index 8080b0d4b1a..d330684c1d5 100644 --- a/src/commands/firestore-backups-list.ts +++ b/src/commands/firestore-backups-list.ts @@ -1,5 +1,4 @@ import { Command } from "../command"; -import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; @@ -23,17 +22,15 @@ export const command = new Command("firestore:backups:list") const listBackupsResponse: ListBackupsResponse = await listBackups(options.project, location); const backups: Backup[] = listBackupsResponse.backups || []; - if (options.json) { - logger.info(JSON.stringify(listBackupsResponse, undefined, 2)); - } else { - printer.prettyPrintBackups(backups); - if (listBackupsResponse.unreachable && listBackupsResponse.unreachable.length > 0) { - logWarning( - "We were not able to reach the following locations: " + - listBackupsResponse.unreachable.join(", "), - ); - } + printer.prettyPrintBackups(backups); + if (listBackupsResponse.unreachable && listBackupsResponse.unreachable.length > 0) { + logWarning( + "We were not able to reach the following locations: " + + listBackupsResponse.unreachable.join(", "), + ); } + // TODO: Consider returning listBackupResponse instead for --json. This will + // be a breaking change but exposes .unreachable, not just .backups. return backups; }); diff --git a/src/commands/firestore-backups-schedules-create.ts b/src/commands/firestore-backups-schedules-create.ts index 255c190b0c0..e7e9e3917d1 100644 --- a/src/commands/firestore-backups-schedules-create.ts +++ b/src/commands/firestore-backups-schedules-create.ts @@ -81,13 +81,9 @@ export const command = new Command("firestore:backups:schedules:create") weeklyRecurrence, ); - if (options.json) { - logger.info(JSON.stringify(backupSchedule, undefined, 2)); - } else { - logger.info( - clc.bold(`Successfully created ${printer.prettyBackupScheduleString(backupSchedule)}`), - ); - } + logger.info( + clc.bold(`Successfully created ${printer.prettyBackupScheduleString(backupSchedule)}`), + ); return backupSchedule; }); diff --git a/src/commands/firestore-backups-schedules-delete.ts b/src/commands/firestore-backups-schedules-delete.ts index 8915f737470..4bb134bd22c 100644 --- a/src/commands/firestore-backups-schedules-delete.ts +++ b/src/commands/firestore-backups-schedules-delete.ts @@ -36,11 +36,7 @@ export const command = new Command("firestore:backups:schedules:delete ") const databaseResp: types.DatabaseResp = await api.createDatabase(createDatabaseReq); - if (options.json) { - logger.info(JSON.stringify(databaseResp, undefined, 2)); - } else { - logger.info(clc.bold(`Successfully created ${printer.prettyDatabaseString(databaseResp)}`)); - logger.info( - "Please be sure to configure Firebase rules in your Firebase config file for\n" + - "the new database. By default, created databases will have closed rules that\n" + - "block any incoming third-party traffic.", - ); - logger.info( - `Your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, database)}`, - ); - } + logger.info(clc.bold(`Successfully created ${printer.prettyDatabaseString(databaseResp)}`)); + logger.info( + "Please be sure to configure Firebase rules in your Firebase config file for\n" + + "the new database. By default, created databases will have closed rules that\n" + + "block any incoming third-party traffic.", + ); + logger.info( + `Your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, database)}`, + ); return databaseResp; }); diff --git a/src/commands/firestore-databases-delete.ts b/src/commands/firestore-databases-delete.ts index 5ae410279be..72d4dd4f6a5 100644 --- a/src/commands/firestore-databases-delete.ts +++ b/src/commands/firestore-databases-delete.ts @@ -31,11 +31,7 @@ export const command = new Command("firestore:databases:delete ") const databaseResp: types.DatabaseResp = await api.deleteDatabase(options.project, database); - if (options.json) { - logger.info(JSON.stringify(databaseResp, undefined, 2)); - } else { - logger.info(clc.bold(`Successfully deleted ${printer.prettyDatabaseString(databaseResp)}`)); - } + logger.info(clc.bold(`Successfully deleted ${printer.prettyDatabaseString(databaseResp)}`)); return databaseResp; }); diff --git a/src/commands/firestore-databases-get.ts b/src/commands/firestore-databases-get.ts index afe2ed0e9bb..e46e2661570 100644 --- a/src/commands/firestore-databases-get.ts +++ b/src/commands/firestore-databases-get.ts @@ -1,7 +1,6 @@ import { Command } from "../command"; import * as fsi from "../firestore/api"; import * as types from "../firestore/api-types"; -import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; @@ -19,11 +18,7 @@ export const command = new Command("firestore:databases:get [database]") const databaseId = database || "(default)"; const databaseResp: types.DatabaseResp = await api.getDatabase(options.project, databaseId); - if (options.json) { - logger.info(JSON.stringify(databaseResp, undefined, 2)); - } else { - printer.prettyPrintDatabase(databaseResp); - } + printer.prettyPrintDatabase(databaseResp); return databaseResp; }); diff --git a/src/commands/firestore-databases-list.ts b/src/commands/firestore-databases-list.ts index 07c50d4180f..d1f1f199634 100644 --- a/src/commands/firestore-databases-list.ts +++ b/src/commands/firestore-databases-list.ts @@ -1,7 +1,6 @@ import { Command } from "../command"; import * as fsi from "../firestore/api"; import * as types from "../firestore/api-types"; -import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; @@ -18,11 +17,7 @@ export const command = new Command("firestore:databases:list") const databases: types.DatabaseResp[] = await api.listDatabases(options.project); - if (options.json) { - logger.info(JSON.stringify(databases, undefined, 2)); - } else { - printer.prettyPrintDatabases(databases); - } + printer.prettyPrintDatabases(databases); return databases; }); diff --git a/src/commands/firestore-databases-restore.ts b/src/commands/firestore-databases-restore.ts index 6cd278248e8..efcb87fa2cb 100644 --- a/src/commands/firestore-databases-restore.ts +++ b/src/commands/firestore-databases-restore.ts @@ -72,21 +72,17 @@ export const command = new Command("firestore:databases:restore") encryptionConfig, ); - if (options.json) { - logger.info(JSON.stringify(databaseResp, undefined, 2)); - } else { - logger.info( - clc.bold(`Successfully initiated restore of ${printer.prettyDatabaseString(databaseResp)}`), - ); - logger.info( - "Please be sure to configure Firebase rules in your Firebase config file for\n" + - "the new database. By default, created databases will have closed rules that\n" + - "block any incoming third-party traffic.", - ); - logger.info( - `Once the restore is complete, your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, databaseId)}`, - ); - } + logger.info( + clc.bold(`Successfully initiated restore of ${printer.prettyDatabaseString(databaseResp)}`), + ); + logger.info( + "Please be sure to configure Firebase rules in your Firebase config file for\n" + + "the new database. By default, created databases will have closed rules that\n" + + "block any incoming third-party traffic.", + ); + logger.info( + `Once the restore is complete, your database may be viewed at ${printer.firebaseConsoleDatabaseUrl(options.project, databaseId)}`, + ); return databaseResp; diff --git a/src/commands/firestore-databases-update.ts b/src/commands/firestore-databases-update.ts index 88ca73c1063..609ba4719df 100644 --- a/src/commands/firestore-databases-update.ts +++ b/src/commands/firestore-databases-update.ts @@ -71,11 +71,7 @@ export const command = new Command("firestore:databases:update ") pointInTimeRecoveryEnablement, ); - if (options.json) { - logger.info(JSON.stringify(databaseResp, undefined, 2)); - } else { - logger.info(clc.bold(`Successfully updated ${printer.prettyDatabaseString(databaseResp)}`)); - } + logger.info(clc.bold(`Successfully updated ${printer.prettyDatabaseString(databaseResp)}`)); return databaseResp; }); diff --git a/src/commands/firestore-locations.ts b/src/commands/firestore-locations.ts index 4cfffcb2a1d..1fc617e7dc0 100644 --- a/src/commands/firestore-locations.ts +++ b/src/commands/firestore-locations.ts @@ -1,7 +1,6 @@ import { Command } from "../command"; import * as fsi from "../firestore/api"; import * as types from "../firestore/api-types"; -import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; @@ -18,11 +17,7 @@ export const command = new Command("firestore:locations") const locations: types.Location[] = await api.locations(options.project); - if (options.json) { - logger.info(JSON.stringify(locations, undefined, 2)); - } else { - printer.prettyPrintLocations(locations); - } + printer.prettyPrintLocations(locations); return locations; }); diff --git a/src/commands/firestore-operations-cancel.spec.ts b/src/commands/firestore-operations-cancel.spec.ts index 89dabc2bfe1..18f015c0318 100644 --- a/src/commands/firestore-operations-cancel.spec.ts +++ b/src/commands/firestore-operations-cancel.spec.ts @@ -4,7 +4,6 @@ import { command } from "./firestore-operations-cancel"; import * as fsi from "../firestore/api"; import * as prompt from "../prompt"; import * as utils from "../utils"; -import { logger } from "../logger"; describe("firestore:operations:cancel", () => { const sandbox = sinon.createSandbox(); @@ -12,7 +11,6 @@ describe("firestore:operations:cancel", () => { let confirmStub: sinon.SinonStub; let logSuccessStub: sinon.SinonStub; let logWarningStub: sinon.SinonStub; - let loggerInfoStub: sinon.SinonStub; beforeEach(() => { firestoreApiStub = sandbox.createStubInstance(fsi.FirestoreApi); @@ -20,7 +18,6 @@ describe("firestore:operations:cancel", () => { confirmStub = sandbox.stub(prompt, "confirm"); logSuccessStub = sandbox.stub(utils, "logSuccess"); logWarningStub = sandbox.stub(utils, "logWarning"); - loggerInfoStub = sandbox.stub(logger, "info"); }); afterEach(() => { @@ -87,10 +84,8 @@ describe("firestore:operations:cancel", () => { const status = { success: true }; firestoreApiStub.cancelOperation.resolves(status); - await command.runner()(operationName, options); + const jsonResult = await command.runner()(operationName, options); - expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(status, undefined, 2)); - expect(logSuccessStub).to.not.be.called; - expect(logWarningStub).to.not.be.called; + expect(jsonResult).to.eql(status); }); }); diff --git a/src/commands/firestore-operations-cancel.ts b/src/commands/firestore-operations-cancel.ts index 5fb158d6b1c..02d2a2c36f6 100644 --- a/src/commands/firestore-operations-cancel.ts +++ b/src/commands/firestore-operations-cancel.ts @@ -7,7 +7,6 @@ import { getShortOperationName } from "./firestore-utils"; import { confirm } from "../prompt"; import * as clc from "colorette"; import * as utils from "../utils"; -import { logger } from "../logger"; export const command = new Command("firestore:operations:cancel ") .description("cancels a long-running Cloud Firestore admin operation") @@ -34,14 +33,10 @@ export const command = new Command("firestore:operations:cancel " const api = new fsi.FirestoreApi(); const status = await api.cancelOperation(options.project, databaseId, operationName); - if (options.json) { - logger.info(JSON.stringify(status, undefined, 2)); + if (status.success) { + utils.logSuccess("Operation cancelled successfully."); } else { - if (status.success) { - utils.logSuccess("Operation cancelled successfully."); - } else { - utils.logWarning("Canceling the operation failed."); - } + utils.logWarning("Canceling the operation failed."); } return status; diff --git a/src/commands/firestore-operations-describe.spec.ts b/src/commands/firestore-operations-describe.spec.ts index bcb371b596c..23af2174079 100644 --- a/src/commands/firestore-operations-describe.spec.ts +++ b/src/commands/firestore-operations-describe.spec.ts @@ -57,10 +57,9 @@ describe("firestore:operations:describe", () => { const operation = { name: "op1", done: false, metadata: {} }; firestoreApiStub.describeOperation.resolves(operation); - await command.runner()(operationName, options); + const jsonResult = await command.runner()(operationName, options); - expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(operation, undefined, 2)); - expect(prettyPrintStub).to.not.be.called; + expect(jsonResult).to.eql(operation); }); it("should pretty-print the operation when --json is not specified", async () => { diff --git a/src/commands/firestore-operations-describe.ts b/src/commands/firestore-operations-describe.ts index db411831af5..7c13d9e26ac 100644 --- a/src/commands/firestore-operations-describe.ts +++ b/src/commands/firestore-operations-describe.ts @@ -1,6 +1,5 @@ import { Command } from "../command"; import * as fsi from "../firestore/api"; -import { logger } from "../logger"; import { Emulators } from "../emulator/types"; import { errorMissingProject, warnEmulatorNotSupported } from "../emulator/commandUtils"; import { FirestoreOptions } from "../firestore/options"; @@ -21,12 +20,8 @@ export const command = new Command("firestore:operations:describe { ]; firestoreApiStub.listOperations.resolves({ operations }); - await command.runner()(options); - - expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(operations, undefined, 2)); - expect(prettyPrintStub).to.not.be.called; + const jsonResult = await command.runner()(options); + expect(jsonResult).to.eql(operations); }); it("should pretty-print operations when --json is not specified", async () => { diff --git a/src/commands/firestore-operations-list.ts b/src/commands/firestore-operations-list.ts index dc549de8fc7..d87e03fc4ee 100644 --- a/src/commands/firestore-operations-list.ts +++ b/src/commands/firestore-operations-list.ts @@ -1,6 +1,5 @@ import { Command } from "../command"; import * as fsi from "../firestore/api"; -import { logger } from "../logger"; import { Emulators } from "../emulator/types"; import { errorMissingProject, warnEmulatorNotSupported } from "../emulator/commandUtils"; import { FirestoreOptions } from "../firestore/options"; @@ -22,12 +21,8 @@ export const command = new Command("firestore:operations:list") const api = new fsi.FirestoreApi(); const { operations } = await api.listOperations(options.project, databaseId, limit); - if (options.json) { - logger.info(JSON.stringify(operations, undefined, 2)); - } else { - const printer = new PrettyPrint(); - printer.prettyPrintOperations(operations); - } + const printer = new PrettyPrint(); + printer.prettyPrintOperations(operations); return operations; }); diff --git a/src/deploy/apphosting/deploy.spec.ts b/src/deploy/apphosting/deploy.spec.ts index d7fb63545b6..b5300bcbe87 100644 --- a/src/deploy/apphosting/deploy.spec.ts +++ b/src/deploy/apphosting/deploy.spec.ts @@ -19,7 +19,6 @@ const BASE_OPTS = { debug: false, filteredTargets: [], rc: new RC(), - json: false, }; function initializeContext(): Context { diff --git a/src/deploy/apphosting/prepare.spec.ts b/src/deploy/apphosting/prepare.spec.ts index b3737a447e5..b4dc21d99d2 100644 --- a/src/deploy/apphosting/prepare.spec.ts +++ b/src/deploy/apphosting/prepare.spec.ts @@ -21,7 +21,6 @@ const BASE_OPTS = { debug: false, filteredTargets: [], rc: new RC(), - json: false, }; function initializeContext(): Context { diff --git a/src/deploy/apphosting/release.spec.ts b/src/deploy/apphosting/release.spec.ts index 0695f99fd2c..ea8433e89b2 100644 --- a/src/deploy/apphosting/release.spec.ts +++ b/src/deploy/apphosting/release.spec.ts @@ -16,7 +16,6 @@ const BASE_OPTS = { debug: false, filteredTargets: [], rc: new RC(), - json: false, }; describe("apphosting", () => { diff --git a/src/deploy/functions/prompts.spec.ts b/src/deploy/functions/prompts.spec.ts index 61fb3681bbd..a418e7184fa 100644 --- a/src/deploy/functions/prompts.spec.ts +++ b/src/deploy/functions/prompts.spec.ts @@ -35,7 +35,6 @@ const SAMPLE_OPTIONS: Options = { only: "functions", except: "", nonInteractive: false, - json: false, interactive: false, debug: false, force: false, diff --git a/src/deploy/hosting/prepare.spec.ts b/src/deploy/hosting/prepare.spec.ts index e1579990594..d1767dc2176 100644 --- a/src/deploy/hosting/prepare.spec.ts +++ b/src/deploy/hosting/prepare.spec.ts @@ -62,7 +62,6 @@ describe("hosting prepare", () => { except: "", filteredTargets: ["HOSTING"], force: false, - json: false, nonInteractive: false, interactive: true, debug: false, diff --git a/src/emulator/extensions/validation.spec.ts b/src/emulator/extensions/validation.spec.ts index 5e4a95c9a0c..5db83d17eb0 100644 --- a/src/emulator/extensions/validation.spec.ts +++ b/src/emulator/extensions/validation.spec.ts @@ -21,7 +21,6 @@ const TEST_OPTIONS: Options = { filteredTargets: [""], nonInteractive: true, interactive: false, - json: false, debug: false, rc: new RC(), config: new Config("."), diff --git a/src/emulator/storage/rules/config.spec.ts b/src/emulator/storage/rules/config.spec.ts index dc57cfd431f..b0dca1693df 100644 --- a/src/emulator/storage/rules/config.spec.ts +++ b/src/emulator/storage/rules/config.spec.ts @@ -127,7 +127,6 @@ function getOptions(config: any): Options { only: "", except: "", nonInteractive: false, - json: false, interactive: false, debug: false, force: false, diff --git a/src/filterTargets.spec.ts b/src/filterTargets.spec.ts index 43e1316977d..6c81742143c 100644 --- a/src/filterTargets.spec.ts +++ b/src/filterTargets.spec.ts @@ -11,7 +11,6 @@ const SAMPLE_OPTIONS: Options = { only: "", except: "", nonInteractive: false, - json: false, interactive: false, debug: false, force: false, diff --git a/src/gcp/cloudsql/cloudsqladmin.spec.ts b/src/gcp/cloudsql/cloudsqladmin.spec.ts index 03ecd10be99..b559b055697 100644 --- a/src/gcp/cloudsql/cloudsqladmin.spec.ts +++ b/src/gcp/cloudsql/cloudsqladmin.spec.ts @@ -26,7 +26,6 @@ const options: Options = { config: new Config({}, { projectDir: "", cwd: "" }), filteredTargets: [], force: false, - json: false, nonInteractive: false, interactive: false, debug: false, diff --git a/src/init/features/functions.spec.ts b/src/init/features/functions.spec.ts index d1650ebd87e..cbe16f2afd9 100644 --- a/src/init/features/functions.spec.ts +++ b/src/init/features/functions.spec.ts @@ -53,7 +53,6 @@ describe("functions", () => { except: "", filteredTargets: [], force: false, - json: false, nonInteractive: false, interactive: false, debug: false, diff --git a/src/init/features/genkit/index.spec.ts b/src/init/features/genkit/index.spec.ts index a7617a3ccdc..00bb7266e50 100644 --- a/src/init/features/genkit/index.spec.ts +++ b/src/init/features/genkit/index.spec.ts @@ -42,7 +42,6 @@ describe("genkit", () => { except: "", filteredTargets: [], force: false, - json: false, nonInteractive: false, interactive: false, debug: false, diff --git a/src/management/studio.spec.ts b/src/management/studio.spec.ts index f6179847e3e..3f105f1c249 100644 --- a/src/management/studio.spec.ts +++ b/src/management/studio.spec.ts @@ -35,7 +35,6 @@ describe("Studio Management", () => { except: "", filteredTargets: [], force: false, - json: false, nonInteractive: false, interactive: false, debug: false, diff --git a/src/options.ts b/src/options.ts index 7374ef1b688..18af6a67549 100644 --- a/src/options.ts +++ b/src/options.ts @@ -19,7 +19,6 @@ export interface BaseOptions { projectNumber?: string; projectRoot?: string; account?: string; - json: boolean; nonInteractive: boolean; interactive: boolean; debug: boolean; @@ -30,6 +29,20 @@ export interface BaseOptions { import?: string; isMCP?: boolean; + + /** + * Do not use this field when handling --json. It is never set in commands. + * + * Instead, return an object to be JSONified from the command action callback: + * + * ```typescript + * .action(async (options: Options) => { + * logger.info('Normal output'); // Automatically suppressed with --json. + * return objectToBePrintedWhenTheJsonFlagIsPassed; + * }); + * ``` + */ + json?: undefined; } export interface Options extends BaseOptions { diff --git a/src/requireConfig.spec.ts b/src/requireConfig.spec.ts index cbdfe4dd08c..023b9aeb9a5 100644 --- a/src/requireConfig.spec.ts +++ b/src/requireConfig.spec.ts @@ -14,7 +14,6 @@ const options: Options = { config: new Config({}), filteredTargets: [], force: false, - json: false, nonInteractive: false, interactive: false, debug: false, diff --git a/src/requireTosAcceptance.spec.ts b/src/requireTosAcceptance.spec.ts index 0e5c3d333d9..416c0807906 100644 --- a/src/requireTosAcceptance.spec.ts +++ b/src/requireTosAcceptance.spec.ts @@ -15,7 +15,6 @@ const SAMPLE_OPTIONS: Options = { only: "", except: "", nonInteractive: false, - json: false, interactive: false, debug: false, force: false, From f29c7ec436a4449c3906fe65d44c89951b9a7944 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 14:11:08 -0700 Subject: [PATCH 03/10] feat(dataconnect): Add confirmation for Gemini schema generation (#9282) * feat(dataconnect): add confirmation for Gemini schema generation Instead of directly asking for an app description to generate a schema with Gemini, this change first asks the user to confirm if they want to use Gemini. If the user confirms, it then prompts for the app description with a default value of "an app for ${setup.projectId}". * prompts * changelog * m * feedback * typo * metrics * Update index.ts --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + src/init/features/dataconnect/index.ts | 27 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..4ffa9bd0696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add a confirmation in `firebase init dataconnect` before asking for app idea description. (#9282) diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts index 66ab9543271..20d072e99cc 100644 --- a/src/init/features/dataconnect/index.ts +++ b/src/init/features/dataconnect/index.ts @@ -118,12 +118,22 @@ export async function askQuestions(setup: Setup): Promise { "Learn more about Gemini in Firebase and how it uses your data: https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data", ); } - info.appDescription = await input({ - message: `Describe your app to automatically generate a schema with Gemini [Enter to use a template]:`, + const wantToGenerate = await confirm({ + message: "Do you want to generate schema and queries with Gemini?", + default: false, }); - if (info.appDescription) { + if (wantToGenerate) { configstore.set("gemini", true); await ensureGIFApiTos(setup.projectId); + info.appDescription = await input({ + message: `Describe your app idea:`, + validate: async (s: string) => { + if (s.length > 0) { + return true; + } + return "Please enter a description for your app idea."; + }, + }); } } if (hasBilling) { @@ -161,9 +171,14 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi await sdk.actuate(setup, config); } finally { void trackGA4("dataconnect_init", { - project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing", flow: info.analyticsFlow, - provision_cloud_sql: String(info.shouldProvisionCSQL), + project_status: setup.projectId + ? setup.isBillingEnabled + ? info.shouldProvisionCSQL + ? "blaze_provisioned_csql" + : "blaze" + : "spark" + : "missing", }); } @@ -646,7 +661,7 @@ async function promptForLocation(setup: Setup, info: RequiredInfo): Promise({ - message: "What location should the new Cloud SQL instance be in?", + message: "What location would you like to use?", choices, default: FDC_DEFAULT_REGION, }); From 0ec2d7da89c7ab08af00944adfd1869a246f2045 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:21:59 +0000 Subject: [PATCH 04/10] feat(dataconnect): Update dataconnect:* commands to use flags Updated all `dataconnect:*` commands that previously used a positional argument for the service ID to use `--service` and `--location` flags instead. This change improves the usability and consistency of the CLI. The following commands were updated: - `dataconnect:sql:migrate` - `dataconnect:sql:diff` - `dataconnect:sdk:generate` - `dataconnect:sql:grant` - `dataconnect:sql:setup` - `dataconnect:sql:shell` The `pickService` and `loadAll` functions in `src/dataconnect/load.ts` were also updated to support filtering by location. --- npm-shrinkwrap.json | 15 ++++++++------- package.json | 2 +- src/commands/dataconnect-sdk-generate.ts | 14 +++++++++++--- src/commands/dataconnect-sql-diff.ts | 13 ++++++++++--- src/commands/dataconnect-sql-grant.ts | 13 ++++++++++--- src/commands/dataconnect-sql-migrate.ts | 13 ++++++++++--- src/commands/dataconnect-sql-setup.ts | 13 ++++++++++--- src/commands/dataconnect-sql-shell.ts | 13 ++++++++++--- src/dataconnect/load.ts | 15 ++++++++++++--- 9 files changed, 82 insertions(+), 29 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9aa64561c77..3a43b4646a3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -172,7 +172,7 @@ "source-map-support": "^0.5.9", "supertest": "^6.2.3", "swagger2openapi": "^7.0.8", - "ts-node": "^10.4.0", + "ts-node": "^10.9.2", "typescript": "^4.5.4", "typescript-json-schema": "^0.65.1", "vite": "^4.2.1" @@ -20361,9 +20361,10 @@ "dev": true }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -36920,9 +36921,9 @@ "dev": true }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", diff --git a/package.json b/package.json index 3ab1a6b8f3e..bfc70e48eb8 100644 --- a/package.json +++ b/package.json @@ -262,7 +262,7 @@ "source-map-support": "^0.5.9", "supertest": "^6.2.3", "swagger2openapi": "^7.0.8", - "ts-node": "^10.4.0", + "ts-node": "^10.9.2", "typescript": "^4.5.4", "typescript-json-schema": "^0.65.1", "vite": "^4.2.1" diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index 741b74370b4..afcd9ca53c5 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -4,24 +4,32 @@ import { Command } from "../command"; import { Options } from "../options"; import { DataConnectEmulator } from "../emulator/dataconnectEmulator"; import { needProjectId } from "../projectUtils"; -import { loadAll } from "../dataconnect/load"; +import { loadAll, pickService } 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 }; export const command = new Command("dataconnect:sdk:generate") .description("generate typed SDKs for your Data Connect connectors") + .option( + "--service ", + "the serviceId of the Data Connect service. If not provided, generates SDKs for all services.", + ) + .option("--location ", "the location of the Data Connect service", "us-central1") .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 location = options.location as string; - const serviceInfos = await loadAll(projectId, options.config); + const serviceInfos = options.service + ? [await pickService(projectId, options.config, options.service, location)] + : await loadAll(projectId, options.config, location); const serviceInfosWithSDKs = serviceInfos.filter((serviceInfo) => serviceInfo.connectorInfo.some((c) => { return ( diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index 73035ad52e8..c1851ce821c 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -7,20 +7,27 @@ import { pickService } from "../dataconnect/load"; import { diffSchema } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; -export const command = new Command("dataconnect:sql:diff [serviceId]") +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", ) + .option("--service ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service", "us-central1") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", "firebasedataconnect.schemas.update", ]) .before(requireAuth) - .action(async (serviceId: string, options: Options) => { + .action(async (options: Options) => { const projectId = needProjectId(options); + if (!options.service) { + throw new FirebaseError("Missing required flag --service"); + } + const serviceId = options.service as string; + const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId); + const serviceInfo = await pickService(projectId, options.config, serviceId, location); const diffs = await diffSchema( options, diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index 30a25da9e9e..a5a73498640 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -12,8 +12,10 @@ import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin"; const allowedRoles = Object.keys(fdcSqlRoleMap); -export const command = new Command("dataconnect:sql:grant [serviceId]") +export const command = new Command("dataconnect:sql:grant") .description("grants the SQL role to the provided user or service account ") + .option("--service ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service", "us-central1") .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") .option( "-E, --email ", @@ -21,7 +23,12 @@ export const command = new Command("dataconnect:sql:grant [serviceId]") ) .before(requirePermissions, ["firebasedataconnect.services.list"]) .before(requireAuth) - .action(async (serviceId: string, options: Options) => { + .action(async (options: Options) => { + if (!options.service) { + throw new FirebaseError("Missing required flag --service"); + } + const serviceId = options.service as string; + const location = options.location as string; const role = options.role as string; const email = options.email as string; if (!role) { @@ -49,7 +56,7 @@ export const command = new Command("dataconnect:sql:grant [serviceId]") const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId); + const serviceInfo = await pickService(projectId, options.config, serviceId, location); await grantRoleToUserInSchema(options, serviceInfo.schema); return { projectId, serviceId }; diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index bcc72943327..d58f157728e 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -9,8 +9,10 @@ import { requirePermissions } from "../requirePermissions"; import { ensureApis } from "../dataconnect/ensureApis"; import { logLabeledSuccess } from "../utils"; -export const command = new Command("dataconnect:sql:migrate [serviceId]") +export const command = new Command("dataconnect:sql:migrate") .description("migrate your CloudSQL database's schema to match your local Data Connect schema") + .option("--service ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service", "us-central1") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", @@ -19,10 +21,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: Options) => { const projectId = needProjectId(options); + if (!options.service) { + throw new FirebaseError("Missing required flag --service"); + } + const serviceId = options.service as string; + const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId); + const serviceInfo = await pickService(projectId, options.config, serviceId, location); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; if (!instanceId) { diff --git a/src/commands/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts index 164a1d55d53..a739dc9fda8 100644 --- a/src/commands/dataconnect-sql-setup.ts +++ b/src/commands/dataconnect-sql-setup.ts @@ -11,8 +11,10 @@ import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions"; import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconnect/schemaMigration"; import { setupIAMUsers } from "../gcp/cloudsql/connect"; -export const command = new Command("dataconnect:sql:setup [serviceId]") +export const command = new Command("dataconnect:sql:setup") .description("set up your CloudSQL database") + .option("--service ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service", "us-central1") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", @@ -20,10 +22,15 @@ export const command = new Command("dataconnect:sql:setup [serviceId]") "cloudsql.instances.connect", ]) .before(requireAuth) - .action(async (serviceId: string, options: Options) => { + .action(async (options: Options) => { const projectId = needProjectId(options); + if (!options.service) { + throw new FirebaseError("Missing required flag --service"); + } + const serviceId = options.service as string; + const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId); + const serviceInfo = await pickService(projectId, options.config, serviceId, location); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; if (!instanceId) { diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index 6023fb44905..ceb3b971f44 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -81,16 +81,23 @@ async function mainShellLoop(conn: pg.PoolClient) { } } -export const command = new Command("dataconnect:sql:shell [serviceId]") +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 ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service", "us-central1") .before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"]) .before(requireAuth) - .action(async (serviceId: string, options: Options) => { + .action(async (options: Options) => { const projectId = needProjectId(options); + if (!options.service) { + throw new FirebaseError("Missing required flag --service"); + } + const serviceId = options.service as string; + const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId); + const serviceInfo = await pickService(projectId, options.config, serviceId, location); const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema); const { user: username } = await getIAMUser(options); const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId); diff --git a/src/dataconnect/load.ts b/src/dataconnect/load.ts index 4f021746e22..5a5beaa4d1d 100644 --- a/src/dataconnect/load.ts +++ b/src/dataconnect/load.ts @@ -21,8 +21,9 @@ export async function pickService( projectId: string, config: Config, serviceId?: string, + location?: string, ): Promise { - const serviceInfos = await loadAll(projectId, config); + const serviceInfos = await loadAll(projectId, config, location); if (serviceInfos.length === 0) { throw new FirebaseError( "No Data Connect services found in firebase.json." + @@ -58,9 +59,17 @@ export async function pickService( /** * Loads all Data Connect service configurations from the firebase.json file. */ -export async function loadAll(projectId: string, config: Config): Promise { +export async function loadAll( + projectId: string, + config: Config, + location?: string, +): Promise { const serviceCfgs = readFirebaseJson(config); - return await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source))); + let serviceInfos = await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source))); + if (location) { + serviceInfos = serviceInfos.filter((si) => si.dataConnectYaml.location === location); + } + return serviceInfos; } /** From d6b9f3f53d70117bf4e9eabe16fd85aa31f71ffa Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 15:27:44 -0700 Subject: [PATCH 05/10] --location is optional --- npm-shrinkwrap.json | 15 +++++++-------- package.json | 2 +- src/commands/dataconnect-sdk-generate.ts | 2 +- src/commands/dataconnect-sql-diff.ts | 3 ++- src/commands/dataconnect-sql-grant.ts | 2 +- src/commands/dataconnect-sql-migrate.ts | 2 +- src/commands/dataconnect-sql-setup.ts | 2 +- src/commands/dataconnect-sql-shell.ts | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3a43b4646a3..9aa64561c77 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -172,7 +172,7 @@ "source-map-support": "^0.5.9", "supertest": "^6.2.3", "swagger2openapi": "^7.0.8", - "ts-node": "^10.9.2", + "ts-node": "^10.4.0", "typescript": "^4.5.4", "typescript-json-schema": "^0.65.1", "vite": "^4.2.1" @@ -20361,10 +20361,9 @@ "dev": true }, "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -36921,9 +36920,9 @@ "dev": true }, "ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", diff --git a/package.json b/package.json index bfc70e48eb8..3ab1a6b8f3e 100644 --- a/package.json +++ b/package.json @@ -262,7 +262,7 @@ "source-map-support": "^0.5.9", "supertest": "^6.2.3", "swagger2openapi": "^7.0.8", - "ts-node": "^10.9.2", + "ts-node": "^10.4.0", "typescript": "^4.5.4", "typescript-json-schema": "^0.65.1", "vite": "^4.2.1" diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index afcd9ca53c5..ec35c228a23 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -18,7 +18,7 @@ export const command = new Command("dataconnect:sdk:generate") "--service ", "the serviceId of the Data Connect service. If not provided, generates SDKs for all services.", ) - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--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", diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index c1851ce821c..7954ee9bb1e 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -6,13 +6,14 @@ import { requirePermissions } from "../requirePermissions"; import { pickService } from "../dataconnect/load"; import { diffSchema } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; +import { FirebaseError } from "../error"; 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", ) .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index a5a73498640..3b504ad0b67 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -15,7 +15,7 @@ const allowedRoles = Object.keys(fdcSqlRoleMap); export const command = new Command("dataconnect:sql:grant") .description("grants the SQL role to the provided user or service account ") .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--location ", "the location of the Data Connect service to disambiguate") .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") .option( "-E, --email ", diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index d58f157728e..7c8df5a46cc 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -12,7 +12,7 @@ import { logLabeledSuccess } from "../utils"; export const command = new Command("dataconnect:sql:migrate") .description("migrate your CloudSQL database's schema to match your local Data Connect schema") .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts index a739dc9fda8..59d58cb94df 100644 --- a/src/commands/dataconnect-sql-setup.ts +++ b/src/commands/dataconnect-sql-setup.ts @@ -14,7 +14,7 @@ import { setupIAMUsers } from "../gcp/cloudsql/connect"; export const command = new Command("dataconnect:sql:setup") .description("set up your CloudSQL database") .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index ceb3b971f44..1a80b50b119 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -86,7 +86,7 @@ export const command = new Command("dataconnect:sql:shell") "start a shell connected directly to your Data Connect service's linked CloudSQL instance", ) .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service", "us-central1") + .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"]) .before(requireAuth) .action(async (options: Options) => { From fe9367a971eddf9d28013a284310ee2d6dd2a02d Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 16:03:31 -0700 Subject: [PATCH 06/10] compile --- src/commands/dataconnect-sdk-generate.ts | 12 +-- src/commands/dataconnect-sql-diff.ts | 14 +--- src/commands/dataconnect-sql-grant.ts | 15 ++-- src/commands/dataconnect-sql-migrate.ts | 11 +-- src/commands/dataconnect-sql-setup.ts | 14 ++-- src/commands/dataconnect-sql-shell.ts | 11 +-- src/dataconnect/load.ts | 81 ++++++++++--------- src/mcp/tools/dataconnect/compile.ts | 18 +++-- src/mcp/tools/dataconnect/execute.ts | 23 ++++-- .../tools/dataconnect/generate_operation.ts | 19 +++-- 10 files changed, 111 insertions(+), 107 deletions(-) diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index ec35c228a23..e3cf03c309e 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -4,16 +4,16 @@ import { Command } from "../command"; import { Options } from "../options"; import { DataConnectEmulator } from "../emulator/dataconnectEmulator"; import { needProjectId } from "../projectUtils"; -import { loadAll, pickService } 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; service?: string }; +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 ", "the serviceId of the Data Connect service. If not provided, generates SDKs for all services.", @@ -25,11 +25,7 @@ export const command = new Command("dataconnect:sdk:generate") ) .action(async (options: GenerateOptions) => { const projectId = needProjectId(options); - const location = options.location as string; - - const serviceInfos = options.service - ? [await pickService(projectId, options.config, options.service, location)] - : await loadAll(projectId, options.config, location); + const serviceInfos = await pickServices(projectId, options.config, options.service, options.location); const serviceInfosWithSDKs = serviceInfos.filter((serviceInfo) => serviceInfo.connectorInfo.some((c) => { return ( diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index 7954ee9bb1e..b603533033c 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -3,14 +3,13 @@ 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"; -import { FirebaseError } from "../error"; 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 ", "the serviceId of the Data Connect service") .option("--location ", "the location of the Data Connect service to disambiguate") @@ -22,18 +21,13 @@ export const command = new Command("dataconnect:sql:diff") .before(requireAuth) .action(async (options: Options) => { const projectId = needProjectId(options); - if (!options.service) { - throw new FirebaseError("Missing required flag --service"); - } - const serviceId = options.service as string; - const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId, location); + 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 }; }); diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index 3b504ad0b67..f80d8003ffa 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -3,7 +3,7 @@ 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 { grantRoleToUserInSchema } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; import { FirebaseError } from "../error"; @@ -14,21 +14,16 @@ const allowedRoles = Object.keys(fdcSqlRoleMap); export const command = new Command("dataconnect:sql:grant") .description("grants the SQL role to the provided user or service account ") - .option("--service ", "the serviceId of the Data Connect service") - .option("--location ", "the location of the Data Connect service to disambiguate") .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") .option( "-E, --email ", "The email of the user or service account we would like to grant the role to.", ) + .option("--service ", "the serviceId of the Data Connect service") + .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, ["firebasedataconnect.services.list"]) .before(requireAuth) .action(async (options: Options) => { - if (!options.service) { - throw new FirebaseError("Missing required flag --service"); - } - const serviceId = options.service as string; - const location = options.location as string; const role = options.role as string; const email = options.email as string; if (!role) { @@ -56,8 +51,8 @@ export const command = new Command("dataconnect:sql:grant") const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId, location); + const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); await grantRoleToUserInSchema(options, serviceInfo.schema); - return { projectId, serviceId }; + return { projectId }; }); diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index 7c8df5a46cc..133b729dcd2 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -1,7 +1,7 @@ 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"; @@ -23,13 +23,8 @@ export const command = new Command("dataconnect:sql:migrate") .withForce("execute any required database changes without prompting") .action(async (options: Options) => { const projectId = needProjectId(options); - if (!options.service) { - throw new FirebaseError("Missing required flag --service"); - } - const serviceId = options.service as string; - const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId, location); + const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; if (!instanceId) { @@ -51,5 +46,5 @@ export const command = new Command("dataconnect:sql:migrate") } else { logLabeledSuccess("dataconnect", "Database schema is already up to date!"); } - return { projectId, serviceId, diffs }; + return { projectId, diffs }; }); diff --git a/src/commands/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts index 59d58cb94df..aed54ae1b38 100644 --- a/src/commands/dataconnect-sql-setup.ts +++ b/src/commands/dataconnect-sql-setup.ts @@ -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"; @@ -10,6 +9,7 @@ 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") .description("set up your CloudSQL database") @@ -24,13 +24,13 @@ export const command = new Command("dataconnect:sql:setup") .before(requireAuth) .action(async (options: Options) => { const projectId = needProjectId(options); - if (!options.service) { - throw new FirebaseError("Missing required flag --service"); - } - const serviceId = options.service as string; - const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId, location); + const serviceInfo = await pickOneService( + projectId, + options.config, + options.service, + options.location, + ); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; if (!instanceId) { diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index 1a80b50b119..cfc17349f00 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -7,7 +7,7 @@ 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 { getIdentifiers } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; import { getIAMUser } from "../gcp/cloudsql/connect"; @@ -91,13 +91,8 @@ export const command = new Command("dataconnect:sql:shell") .before(requireAuth) .action(async (options: Options) => { const projectId = needProjectId(options); - if (!options.service) { - throw new FirebaseError("Missing required flag --service"); - } - const serviceId = options.service as string; - const location = options.location as string; await ensureApis(projectId); - const serviceInfo = await pickService(projectId, options.config, serviceId, location); + 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); @@ -141,5 +136,5 @@ export const command = new Command("dataconnect:sql:shell") await pool.end(); connector.close(); - return { projectId, serviceId }; + return { projectId }; }); diff --git a/src/dataconnect/load.ts b/src/dataconnect/load.ts index 5a5beaa4d1d..605b08f5739 100644 --- a/src/dataconnect/load.ts +++ b/src/dataconnect/load.ts @@ -15,61 +15,64 @@ import { import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; import { DataConnectMultiple } from "../firebaseConfig"; -// pickService reads firebase.json and returns all services with a given serviceId. +export async function pickOneService(projectId: string, config: Config, service?: string, location?: string): Promise { + 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]; +} + +// pickService reads firebase.json and returns a service with a given serviceId and location. // If serviceID is not provided and there is a single service, return that. -export async function pickService( +export async function pickServices( projectId: string, config: Config, serviceId?: string, location?: string, -): Promise { - const serviceInfos = await loadAll(projectId, config, location); +): Promise { + const serviceInfos = await loadAll(projectId, config); if (serviceInfos.length === 0) { + let message = "No Data Connect services found in firebase.json."; + if (location) { + message = `No Data Connect services for location ${location} found in firebase.json.`; + } throw new FirebaseError( - "No Data Connect services found in firebase.json." + + message + `\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(", ")}` + + `\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`, + ); + } + return matchingServices; } /** * Loads all Data Connect service configurations from the firebase.json file. */ -export async function loadAll( - projectId: string, - config: Config, - location?: string, -): Promise { +export async function loadAll(projectId: string, config: Config): Promise { const serviceCfgs = readFirebaseJson(config); - let serviceInfos = await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source))); - if (location) { - serviceInfos = serviceInfos.filter((si) => si.dataConnectYaml.location === location); - } - return serviceInfos; + return await Promise.all(serviceCfgs.map((c) => load(projectId, config, c.source))); } /** diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index e5904385051..d5d02440eed 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { tool } from "../../tool"; -import { pickService } from "../../../dataconnect/load"; import { compileErrors } from "../../util/dataconnect/compile"; +import { pickOneService, pickServices } from "../../../dataconnect/load"; export const compile = tool( { @@ -13,11 +13,15 @@ export const compile = tool( .enum(["all", "schema", "operations"]) .describe("filter errors to a specific type only. defaults to `all` if omitted.") .optional(), - service_id: z + service_id: z.string().optional() + .describe( + `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + ), + location_id: z .string() .optional() .describe( - "The Firebase Data Connect service ID to look for. If omitted, builds all services defined in `firebase.json`.", + `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, ), }), annotations: { @@ -29,9 +33,11 @@ export const compile = tool( requiresAuth: false, }, }, - async ({ service_id, error_filter }, { projectId, config }) => { - const serviceInfo = await pickService(projectId, config, service_id || undefined); - const errors = await compileErrors(serviceInfo.sourceDirectory, error_filter); + async ({ service_id, location_id, error_filter }, { projectId, config }) => { + const serviceInfos = await pickServices(projectId, config, service_id || undefined, location_id || undefined); + const errors = await Promise.all(serviceInfos.map(async (serviceInfo) => { + return await compileErrors(serviceInfo.sourceDirectory, error_filter); + })); if (errors) return { content: [ diff --git a/src/mcp/tools/dataconnect/execute.ts b/src/mcp/tools/dataconnect/execute.ts index 6143c7df01f..07789dd7e70 100644 --- a/src/mcp/tools/dataconnect/execute.ts +++ b/src/mcp/tools/dataconnect/execute.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { tool } from "../../tool"; import * as dataplane from "../../../dataconnect/dataplaneClient"; -import { pickService } from "../../../dataconnect/load"; +import { pickOneService, pickServices } from "../../../dataconnect/load"; import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; import { Client } from "../../../apiv2"; @@ -18,10 +18,15 @@ You can use the \`dataconnect_generate_operation\` tool to generate a query. Example Data Connect schema and example queries can be found in files ending in \`.graphql\` or \`.gql\`. `), service_id: z.string().optional() - .describe(`Data Connect Service ID to dis-ambulate if there are multiple. -It's only necessary if there are multiple dataconnect sources in \`firebase.json\`. -You can find candidate service_id in \`dataconnect.yaml\` -`), + .describe( + `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + ), + location_id: z + .string() + .optional() + .describe( + `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, + ), variables_json: z .string() .optional() @@ -55,13 +60,19 @@ You can find candidate service_id in \`dataconnect.yaml\` { query, service_id, + location_id, variables_json: unparsedVariables, use_emulator, auth_token_json: unparsedAuthToken, }, { projectId, config, host }, ) => { - const serviceInfo = await pickService(projectId, config, service_id || undefined); + const serviceInfo = await pickOneService( + projectId, + config, + service_id || undefined, + location_id || undefined, + ); let apiClient: Client; if (use_emulator) { apiClient = await getDataConnectEmulatorClient(host); diff --git a/src/mcp/tools/dataconnect/generate_operation.ts b/src/mcp/tools/dataconnect/generate_operation.ts index 731afc7adc7..d39e573b9d9 100644 --- a/src/mcp/tools/dataconnect/generate_operation.ts +++ b/src/mcp/tools/dataconnect/generate_operation.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { tool } from "../../tool"; import { toContent } from "../../util"; import { generateOperation } from "../../../gemini/fdcExperience"; -import { pickService } from "../../../dataconnect/load"; +import { pickOneService } from "../../../dataconnect/load"; export const generate_operation = tool( { @@ -16,11 +16,15 @@ export const generate_operation = tool( .describe( "Write the prompt like you're talking to a person, describe the task you're trying to accomplish and give details that are specific to the users request", ), - service_id: z + service_id: z.string().optional() + .describe( + `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + ), + location_id: z .string() .optional() .describe( - "Optional: Uses the service ID from the firebase.json file if nothing provided. The service ID of the deployed Firebase resource.", + `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, ), }), annotations: { @@ -33,8 +37,13 @@ export const generate_operation = tool( requiresGemini: true, }, }, - async ({ prompt, service_id }, { projectId, config }) => { - const serviceInfo = await pickService(projectId, config, service_id || undefined); + async ({ prompt, service_id, location_id }, { projectId, config }) => { + const serviceInfo = await pickOneService( + projectId, + config, + service_id || undefined, + location_id || undefined, + ); const schema = await generateOperation(prompt, serviceInfo.serviceName, projectId); return toContent(schema); }, From 3d80375c4d08ac970cb08d335e35e5f1b2449833 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 16:10:44 -0700 Subject: [PATCH 07/10] format --- src/commands/dataconnect-sdk-generate.ts | 7 ++++- src/commands/dataconnect-sql-diff.ts | 9 +++++-- src/commands/dataconnect-sql-grant.ts | 7 ++++- src/commands/dataconnect-sql-migrate.ts | 7 ++++- src/commands/dataconnect-sql-setup.ts | 4 +-- src/commands/dataconnect-sql-shell.ts | 9 +++++-- src/dataconnect/load.ts | 7 ++++- src/mcp/tools/dataconnect/compile.ts | 27 +++++++++++++------ src/mcp/tools/dataconnect/execute.ts | 6 +++-- .../tools/dataconnect/generate_operation.ts | 4 ++- 10 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index e3cf03c309e..204f25c0f09 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -25,7 +25,12 @@ export const command = new Command("dataconnect:sdk:generate") ) .action(async (options: GenerateOptions) => { const projectId = needProjectId(options); - const serviceInfos = await pickServices(projectId, options.config, options.service, options.location); + const serviceInfos = await pickServices( + projectId, + options.config, + options.service, + options.location, + ); const serviceInfosWithSDKs = serviceInfos.filter((serviceInfo) => serviceInfo.connectorInfo.some((c) => { return ( diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index b603533033c..fe8b0b66e4b 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -22,12 +22,17 @@ export const command = new Command("dataconnect:sql:diff") .action(async (options: Options) => { const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); + const serviceInfo = await pickOneService( + projectId, + options.config, + options.service as string | undefined, + options.location as string | undefined, + ); const diffs = await diffSchema( options, serviceInfo.schema, serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation, ); - return {projectId, diffs }; + return { projectId, diffs }; }); diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index f80d8003ffa..9ba2cacbcfc 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -51,7 +51,12 @@ export const command = new Command("dataconnect:sql:grant") const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); + const serviceInfo = await pickOneService( + projectId, + options.config, + options.service as string | undefined, + options.location as string | undefined, + ); await grantRoleToUserInSchema(options, serviceInfo.schema); return { projectId }; diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index 133b729dcd2..31f3c68c019 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -24,7 +24,12 @@ export const command = new Command("dataconnect:sql:migrate") .action(async (options: Options) => { const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); + const serviceInfo = await pickOneService( + projectId, + options.config, + options.service as string | undefined, + options.location as string | undefined, + ); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; if (!instanceId) { diff --git a/src/commands/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts index aed54ae1b38..2134b6bb702 100644 --- a/src/commands/dataconnect-sql-setup.ts +++ b/src/commands/dataconnect-sql-setup.ts @@ -28,8 +28,8 @@ export const command = new Command("dataconnect:sql:setup") const serviceInfo = await pickOneService( projectId, options.config, - options.service, - options.location, + options.service as string | undefined, + options.location as string | undefined, ); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index cfc17349f00..1e9880785ae 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -7,7 +7,7 @@ import { Options } from "../options"; import { needProjectId } from "../projectUtils"; import { ensureApis } from "../dataconnect/ensureApis"; import { requirePermissions } from "../requirePermissions"; -import { pickOneService } from "../dataconnect/load"; +import { pickOneService } from "../dataconnect/load"; import { getIdentifiers } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; import { getIAMUser } from "../gcp/cloudsql/connect"; @@ -92,7 +92,12 @@ export const command = new Command("dataconnect:sql:shell") .action(async (options: Options) => { const projectId = needProjectId(options); await ensureApis(projectId); - const serviceInfo = await pickOneService(projectId, options.config, options.service, options.location); + const serviceInfo = await pickOneService( + projectId, + options.config, + options.service as string | undefined, + options.location as string | undefined, + ); const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema); const { user: username } = await getIAMUser(options); const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId); diff --git a/src/dataconnect/load.ts b/src/dataconnect/load.ts index 605b08f5739..3b0fce825cc 100644 --- a/src/dataconnect/load.ts +++ b/src/dataconnect/load.ts @@ -15,7 +15,12 @@ import { import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; import { DataConnectMultiple } from "../firebaseConfig"; -export async function pickOneService(projectId: string, config: Config, service?: string, location?: string): Promise { +export async function pickOneService( + projectId: string, + config: Config, + service?: string, + location?: string, +): Promise { const services = await pickServices(projectId, config, service, location); if (services.length > 1) { const serviceIds = services.map( diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index d5d02440eed..bb8a8fddee3 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { tool } from "../../tool"; import { compileErrors } from "../../util/dataconnect/compile"; -import { pickOneService, pickServices } from "../../../dataconnect/load"; +import { pickServices } from "../../../dataconnect/load"; export const compile = tool( { @@ -13,7 +13,9 @@ export const compile = tool( .enum(["all", "schema", "operations"]) .describe("filter errors to a specific type only. defaults to `all` if omitted.") .optional(), - service_id: z.string().optional() + service_id: z + .string() + .optional() .describe( `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, ), @@ -34,16 +36,25 @@ export const compile = tool( }, }, async ({ service_id, location_id, error_filter }, { projectId, config }) => { - const serviceInfos = await pickServices(projectId, config, service_id || undefined, location_id || undefined); - const errors = await Promise.all(serviceInfos.map(async (serviceInfo) => { - return await compileErrors(serviceInfo.sourceDirectory, error_filter); - })); - if (errors) + const serviceInfos = await pickServices( + projectId, + config, + service_id || undefined, + location_id || undefined, + ); + const errors = ( + await Promise.all( + serviceInfos.map(async (serviceInfo) => { + return await compileErrors(serviceInfo.sourceDirectory, error_filter); + }), + ) + ).flat(); + if (errors.length > 0) return { content: [ { type: "text", - text: `The following errors were encountered while compiling Data Connect from directory \`${serviceInfo.sourceDirectory}\`:\n\n${errors}`, + text: `The following errors were encountered while compiling Data Connect:\n\n${errors.join("\n")}`, }, ], isError: true, diff --git a/src/mcp/tools/dataconnect/execute.ts b/src/mcp/tools/dataconnect/execute.ts index 07789dd7e70..8d84b277f78 100644 --- a/src/mcp/tools/dataconnect/execute.ts +++ b/src/mcp/tools/dataconnect/execute.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { tool } from "../../tool"; import * as dataplane from "../../../dataconnect/dataplaneClient"; -import { pickOneService, pickServices } from "../../../dataconnect/load"; +import { pickOneService } from "../../../dataconnect/load"; import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; import { Client } from "../../../apiv2"; @@ -17,7 +17,9 @@ export const execute = tool( You can use the \`dataconnect_generate_operation\` tool to generate a query. Example Data Connect schema and example queries can be found in files ending in \`.graphql\` or \`.gql\`. `), - service_id: z.string().optional() + service_id: z + .string() + .optional() .describe( `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, ), diff --git a/src/mcp/tools/dataconnect/generate_operation.ts b/src/mcp/tools/dataconnect/generate_operation.ts index d39e573b9d9..2a9fcdbb61c 100644 --- a/src/mcp/tools/dataconnect/generate_operation.ts +++ b/src/mcp/tools/dataconnect/generate_operation.ts @@ -16,7 +16,9 @@ export const generate_operation = tool( .describe( "Write the prompt like you're talking to a person, describe the task you're trying to accomplish and give details that are specific to the users request", ), - service_id: z.string().optional() + service_id: z + .string() + .optional() .describe( `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, ), From 4590b18ef762cef32dd3f5ee412048d057cfc034 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 16:13:53 -0700 Subject: [PATCH 08/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ffa9bd0696..e9ba0ca7610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) From 12e4127f57fedfc1401d2dcc794b9a6c5185f541 Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 16:57:57 -0700 Subject: [PATCH 09/10] feedbacks --- src/commands/dataconnect-sql-diff.ts | 8 +++-- src/commands/dataconnect-sql-grant.ts | 33 +++++++++++-------- src/commands/dataconnect-sql-migrate.ts | 8 +++-- src/commands/dataconnect-sql-setup.ts | 8 +++-- src/commands/dataconnect-sql-shell.ts | 8 +++-- src/dataconnect/load.ts | 13 +++----- src/mcp/tools/dataconnect/compile.ts | 4 +-- src/mcp/tools/dataconnect/execute.ts | 4 +-- .../tools/dataconnect/generate_operation.ts | 4 +-- 9 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index fe8b0b66e4b..de12631d884 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -7,6 +7,8 @@ import { pickOneService } from "../dataconnect/load"; import { diffSchema } from "../dataconnect/schemaMigration"; import { requireAuth } from "../requireAuth"; +type DiffOptions = Options & { service?: string; location?: string }; + export const command = new Command("dataconnect:sql:diff") .description( "display the differences between the local Data Connect schema and your CloudSQL database's schema", @@ -19,14 +21,14 @@ export const command = new Command("dataconnect:sql:diff") "firebasedataconnect.schemas.update", ]) .before(requireAuth) - .action(async (options: Options) => { + .action(async (options: DiffOptions) => { const projectId = needProjectId(options); await ensureApis(projectId); const serviceInfo = await pickOneService( projectId, options.config, - options.service as string | undefined, - options.location as string | undefined, + options.service, + options.location, ); const diffs = await diffSchema( diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index 9ba2cacbcfc..808fcd1ca9e 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -12,6 +12,13 @@ import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin"; const allowedRoles = Object.keys(fdcSqlRoleMap); +type GrantOptions = Options & { + role?: string; + email?: string; + service?: string; + location?: string; +}; + export const command = new Command("dataconnect:sql:grant") .description("grants the SQL role to the provided user or service account ") .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") @@ -23,32 +30,22 @@ export const command = new Command("dataconnect:sql:grant") .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, ["firebasedataconnect.services.list"]) .before(requireAuth) - .action(async (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 is required. Run the command with -h for more info.", ); } - if (!email) { + if (!options.email) { throw new FirebaseError( "-E, --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(" | ")}.`); } - // 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}`, - ); - } - const projectId = needProjectId(options); await ensureApis(projectId); const serviceInfo = await pickOneService( @@ -58,6 +55,14 @@ export const command = new Command("dataconnect:sql:grant") options.location as string | undefined, ); + // 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 ${options.role} to ${options.email}`, + ); + } + await grantRoleToUserInSchema(options, serviceInfo.schema); return { projectId }; }); diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index 31f3c68c019..636b89d4988 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -9,6 +9,8 @@ import { requirePermissions } from "../requirePermissions"; import { ensureApis } from "../dataconnect/ensureApis"; import { logLabeledSuccess } from "../utils"; +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 ", "the serviceId of the Data Connect service") @@ -21,14 +23,14 @@ export const command = new Command("dataconnect:sql:migrate") ]) .before(requireAuth) .withForce("execute any required database changes without prompting") - .action(async (options: Options) => { + .action(async (options: MigrateOptions) => { const projectId = needProjectId(options); await ensureApis(projectId); const serviceInfo = await pickOneService( projectId, options.config, - options.service as string | undefined, - options.location as string | undefined, + options.service, + options.location, ); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; diff --git a/src/commands/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts index 2134b6bb702..4627e997b9c 100644 --- a/src/commands/dataconnect-sql-setup.ts +++ b/src/commands/dataconnect-sql-setup.ts @@ -11,6 +11,8 @@ import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconne import { setupIAMUsers } from "../gcp/cloudsql/connect"; import { pickOneService } from "../dataconnect/load"; +type SetupOptions = Options & { service?: string; location?: string }; + export const command = new Command("dataconnect:sql:setup") .description("set up your CloudSQL database") .option("--service ", "the serviceId of the Data Connect service") @@ -22,14 +24,14 @@ export const command = new Command("dataconnect:sql:setup") "cloudsql.instances.connect", ]) .before(requireAuth) - .action(async (options: Options) => { + .action(async (options: SetupOptions) => { const projectId = needProjectId(options); await ensureApis(projectId); const serviceInfo = await pickOneService( projectId, options.config, - options.service as string | undefined, - options.location as string | undefined, + options.service, + options.location, ); const instanceId = serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index 1e9880785ae..85eb716ed21 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -81,6 +81,8 @@ async function mainShellLoop(conn: pg.PoolClient) { } } +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", @@ -89,14 +91,14 @@ export const command = new Command("dataconnect:sql:shell") .option("--location ", "the location of the Data Connect service to disambiguate") .before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"]) .before(requireAuth) - .action(async (options: Options) => { + .action(async (options: ShellOptions) => { const projectId = needProjectId(options); await ensureApis(projectId); const serviceInfo = await pickOneService( projectId, options.config, - options.service as string | undefined, - options.location as string | undefined, + options.service, + options.location, ); const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema); const { user: username } = await getIAMUser(options); diff --git a/src/dataconnect/load.ts b/src/dataconnect/load.ts index 3b0fce825cc..33671e79caa 100644 --- a/src/dataconnect/load.ts +++ b/src/dataconnect/load.ts @@ -15,6 +15,7 @@ import { import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; import { DataConnectMultiple } from "../firebaseConfig"; +/** Picks exactly one Data Connect service based on flags. */ export async function pickOneService( projectId: string, config: Config, @@ -35,8 +36,7 @@ export async function pickOneService( return services[0]; } -// pickService reads firebase.json and returns a service with a given serviceId and location. -// If serviceID is not provided and there is a single service, return that. +/** Picks Data Connect services based on flags. */ export async function pickServices( projectId: string, config: Config, @@ -45,12 +45,8 @@ export async function pickServices( ): Promise { const serviceInfos = await loadAll(projectId, config); if (serviceInfos.length === 0) { - let message = "No Data Connect services found in firebase.json."; - if (location) { - message = `No Data Connect services for location ${location} found in firebase.json.`; - } throw new FirebaseError( - message + + "No Data Connect services found in firebase.json." + `\nYou can run ${clc.bold("firebase init dataconnect")} to add a Data Connect service.`, ); } @@ -65,8 +61,7 @@ export async function pickServices( (i) => `${i.dataConnectYaml.location}:${i.dataConnectYaml.serviceId}`, ); throw new FirebaseError( - `No service matched service in firebase.json. Available services: ${serviceIds.join(", ")}` + - `\nYou can run ${clc.bold("firebase init dataconnect")} to add this Data Connect service.`, + `No service matched service in firebase.json. Available services: ${serviceIds.join(", ")}`, ); } return matchingServices; diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts index bb8a8fddee3..bec43f68357 100644 --- a/src/mcp/tools/dataconnect/compile.ts +++ b/src/mcp/tools/dataconnect/compile.ts @@ -17,13 +17,13 @@ export const compile = tool( .string() .optional() .describe( - `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + `Data Connect Service ID to disambiguate if there are multiple Data Connect services.`, ), location_id: z .string() .optional() .describe( - `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, + `Data Connect Service location ID to disambiguate among multiple Data Connect services.`, ), }), annotations: { diff --git a/src/mcp/tools/dataconnect/execute.ts b/src/mcp/tools/dataconnect/execute.ts index 8d84b277f78..b541a0b64f9 100644 --- a/src/mcp/tools/dataconnect/execute.ts +++ b/src/mcp/tools/dataconnect/execute.ts @@ -21,13 +21,13 @@ Example Data Connect schema and example queries can be found in files ending in .string() .optional() .describe( - `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + `Data Connect Service ID to disambiguate if there are multiple Data Connect services.`, ), location_id: z .string() .optional() .describe( - `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, + `Data Connect Service location ID to disambiguate among multiple Data Connect services.`, ), variables_json: z .string() diff --git a/src/mcp/tools/dataconnect/generate_operation.ts b/src/mcp/tools/dataconnect/generate_operation.ts index 2a9fcdbb61c..309907e83a3 100644 --- a/src/mcp/tools/dataconnect/generate_operation.ts +++ b/src/mcp/tools/dataconnect/generate_operation.ts @@ -20,13 +20,13 @@ export const generate_operation = tool( .string() .optional() .describe( - `Data Connect Service ID to dis-ambulate if there are multiple Data Connect services.`, + `Data Connect Service ID to disambiguate if there are multiple Data Connect services.`, ), location_id: z .string() .optional() .describe( - `Data Connect Service location ID to dis-ambulate among multiple Data Connect services.`, + `Data Connect Service location ID to disambiguate among multiple Data Connect services.`, ), }), annotations: { From 13eebfc11810dd1476cd7be41179664e24fef4ac Mon Sep 17 00:00:00 2001 From: Fred Zhang Date: Mon, 13 Oct 2025 17:13:34 -0700 Subject: [PATCH 10/10] m --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ba0ca7610..0dd9db9ca89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +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) +- Update dataconnect:\* commands to use flags for --service & --location (#9312)