Skip to content

implement --only for emulators:export#10057

Merged
aalej merged 8 commits into
mainfrom
aalej_fix-export-only
Mar 10, 2026
Merged

implement --only for emulators:export#10057
aalej merged 8 commits into
mainfrom
aalej_fix-export-only

Conversation

@aalej
Copy link
Copy Markdown
Contributor

@aalej aalej commented Mar 10, 2026

Description

Fixes #4033

Scenarios Tested

Verified that firebase emulators:export ./emulator-data --only auth only exports auth data
Verified that firebase emulators:export ./emulator-data exports all data

Verified that POST works for exporting

  1. no targets specified
    Running below will export all
curl -X POST http://127.0.0.1:4400/_admin/export -d '{"path": "/Users/<USERNAME>/Desktop/firebase-tools/issues/4033/emulator-data", "initiatedBy": "Miming" }' -H "Content-Type: application/json"
  1. targets specified
    Running below will export only auth
curl -X POST http://127.0.0.1:4400/_admin/export -d '{"path": "/Users/<USERNAME>/Desktop/firebase-toos/issues/4033/emulator-data", "initiatedBy": "Miming", "targets": ["auth"] }' -H "Content-Type: application/json"

Sample Commands

firebase emulators:export ./emulator-data --only auth

@aalej aalej changed the title Aalej fix export only implement --only for emulators:export Mar 10, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the firebase emulators:export command by introducing an --only flag. This new functionality empowers users to selectively export data from specific emulators, such as Authentication or Storage, rather than exporting all available emulator data. The changes involve modifying the core export logic within the emulator controller and hub to correctly interpret and apply the specified targets. Additionally, the test suite has been expanded and refined to ensure the robustness of this new feature and to improve the overall stability of emulator import/export tests.

Highlights

  • New Feature: --only flag for emulators:export: The emulators:export command now supports an --only flag, allowing users to specify which emulator data (e.g., auth, storage) should be exported. This provides granular control over the export process.
  • Enhanced Export Logic: The internal export mechanism has been updated to filter and process only the data for the emulators specified by the --only flag, ensuring efficient and targeted exports.
  • Improved Test Stability: Cleanup logic for Admin SDK instances was added to end-to-end tests, preventing 'Firebase app named already exists' errors and improving test reliability.
  • Comprehensive Testing: New end-to-end tests were introduced to verify the correct behavior of emulators:export both with and without the --only flag, covering scenarios for selective and full data exports.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • CHANGELOG.md
    • Added a changelog entry for the new --only flag in emulators:export.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request implements the --only flag for the emulators:export command, allowing users to selectively export data for specific emulators. This involved modifying the CLI controller, emulator hub, and export logic to filter emulators based on the provided targets. New end-to-end tests were added to validate this functionality, which also included cleanup for admin SDK instances in existing tests. A review comment highlighted significant code duplication within these new test cases, suggesting refactoring into a shared helper function to improve maintainability.

Comment on lines +762 to 988
it("should export all data when `--only` flag isn't used `emulators:export`", async function (this) {
this.timeout(2 * TEST_SETUP_TIMEOUT);
await new Promise((resolve) => setTimeout(resolve, 2000));

// Start up emulator suite
const emulatorsCLI = new CLIProcess("1", __dirname);
await emulatorsCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage,auth"],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

const credPath = path.join(__dirname, "service-account-key.json");
const credential = fs.existsSync(credPath)
? admin.credential.cert(credPath)
: admin.credential.applicationDefault();

const config = readConfig();
const storagePort = config.emulators!.storage.port;
process.env.STORAGE_EMULATOR_HOST = `http://${await localhost()}:${storagePort}`;

// Write some data to export
const aApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
storageBucket: "bucket-a",
credential,
},
"storage-export-a",
);
const bApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
storageBucket: "bucket-b",
credential,
},
"storage-export-b",
);

// Write data to two buckets
await aApp.storage().bucket().file("a/b.txt").save("a/b hello, world!");
await aApp.storage().bucket().file("c/d.txt").save("c/d hello, world!");
await bApp.storage().bucket().file("e/f.txt").save("e/f hello, world!");
await bApp.storage().bucket().file("g/h.txt").save("g/h hello, world!");

// Create some accounts to export:
const authPort = config.emulators!.auth.port;
process.env.FIREBASE_AUTH_EMULATOR_HOST = `${await localhost()}:${authPort}`;
const cApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
credential: ADMIN_CREDENTIAL,
},
"auth-export",
);
await cApp.auth().createUser({ uid: "123", email: "foo@example.com", password: "testing" });
await cApp.auth().createUser({ uid: "456", email: "bar@example.com", emailVerified: true });

// Ask for export
const exportCLI = new CLIProcess("2", __dirname);
const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data"));
await exportCLI.start(
"emulators:export",
FIREBASE_PROJECT,
[exportPath],
logIncludes("Export complete"),
);
await exportCLI.stop();

// Check that the right export files are created
const storageExportPath = path.join(exportPath, "storage_export");
const storageExportFiles = fs.readdirSync(storageExportPath).sort();
expect(storageExportFiles).to.eql(["blobs", "buckets.json", "metadata"]);

// Stop the suite
await emulatorsCLI.stop();

// Attempt to import
const importCLI = new CLIProcess("3", __dirname);
await importCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage,auth", "--import", exportPath],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

// List the files
const [aFiles] = await aApp.storage().bucket().getFiles({
prefix: "a/",
});
const aFileNames = aFiles.map((f) => f.name).sort();
expect(aFileNames).to.eql(["a/b.txt"]);

const [bFiles] = await bApp.storage().bucket().getFiles({
prefix: "e/",
});
const bFileNames = bFiles.map((f) => f.name).sort();
expect(bFileNames).to.eql(["e/f.txt"]);

const user1 = await cApp.auth().getUserByEmail("foo@example.com");
expect(user1.passwordHash).to.match(/:password=testing$/);
const user2 = await cApp.auth().getUserByEmail("bar@example.com");
expect(user2.emailVerified).to.be.true;

await importCLI.stop();

// Clean up the admin sdk instances to prevent "Firebase app named <name> already exists." errors in later tests
await aApp.delete();
await bApp.delete();
await cApp.delete();
});

it("should export only storage data with `emulators:export --only storage`", async function (this) {
this.timeout(2 * TEST_SETUP_TIMEOUT);
await new Promise((resolve) => setTimeout(resolve, 2000));

// Start up emulator suite
const emulatorsCLI = new CLIProcess("1", __dirname);
await emulatorsCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage,auth"],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

const credPath = path.join(__dirname, "service-account-key.json");
const credential = fs.existsSync(credPath)
? admin.credential.cert(credPath)
: admin.credential.applicationDefault();

const config = readConfig();
const storagePort = config.emulators!.storage.port;
process.env.STORAGE_EMULATOR_HOST = `http://${await localhost()}:${storagePort}`;

// Write some data to export
const aApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
storageBucket: "bucket-a",
credential,
},
"storage-export-a",
);
const bApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
storageBucket: "bucket-b",
credential,
},
"storage-export-b",
);

// Write data to two buckets
await aApp.storage().bucket().file("a/b.txt").save("a/b hello, world!");
await aApp.storage().bucket().file("c/d.txt").save("c/d hello, world!");
await bApp.storage().bucket().file("e/f.txt").save("e/f hello, world!");
await bApp.storage().bucket().file("g/h.txt").save("g/h hello, world!");

// Create some accounts to export:
const authPort = config.emulators!.auth.port;
process.env.FIREBASE_AUTH_EMULATOR_HOST = `${await localhost()}:${authPort}`;
const cApp = admin.initializeApp(
{
projectId: FIREBASE_PROJECT,
credential: ADMIN_CREDENTIAL,
},
"auth-export",
);
await cApp.auth().createUser({ uid: "123", email: "foo@example.com", password: "testing" });
await cApp.auth().createUser({ uid: "456", email: "bar@example.com", emailVerified: true });

// Ask for export
const exportCLI = new CLIProcess("2", __dirname);
const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data"));
await exportCLI.start(
"emulators:export",
FIREBASE_PROJECT,
[exportPath, "--only", "storage"],
logIncludes("Export complete"),
);
await exportCLI.stop();

// Check that the right export files are created
const storageExportPath = path.join(exportPath, "storage_export");
const storageExportFiles = fs.readdirSync(storageExportPath).sort();
expect(storageExportFiles).to.eql(["blobs", "buckets.json", "metadata"]);

// Stop the suite
await emulatorsCLI.stop();

// Attempt to import
const importCLI = new CLIProcess("3", __dirname);
await importCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage,auth", "--import", exportPath],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

// List the files
const [aFiles] = await aApp.storage().bucket().getFiles({
prefix: "a/",
});
const aFileNames = aFiles.map((f) => f.name).sort();
expect(aFileNames).to.eql(["a/b.txt"]);

const [bFiles] = await bApp.storage().bucket().getFiles({
prefix: "e/",
});
const bFileNames = bFiles.map((f) => f.name).sort();
expect(bFileNames).to.eql(["e/f.txt"]);

await expect(cApp.auth().getUserByEmail("foo@example.com"))
.to.eventually.be.rejectedWith(Error)
.and.have.property("code", "auth/user-not-found");
await expect(cApp.auth().getUserByEmail("bar@example.com"))
.to.eventually.be.rejectedWith(Error)
.and.have.property("code", "auth/user-not-found");

await importCLI.stop();

// Clean up the admin sdk instances to prevent "Firebase app named <name> already exists." errors in later tests
await aApp.delete();
await bApp.delete();
await cApp.delete();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a significant amount of duplicated code between the two new test cases: should export all data when --only flag isn't used emulators:export and should export only storage data with emulators:export --only storage. The setup logic for starting emulators, initializing admin apps, and creating test data is nearly identical.

To improve test maintainability and reduce redundancy, consider refactoring this common setup into a shared helper function. This would make the tests cleaner and easier to manage in the future.

Comment thread CHANGELOG.md Outdated
@aalej aalej merged commit d14584d into main Mar 10, 2026
47 checks passed
@aalej aalej deleted the aalej_fix-export-only branch March 10, 2026 19:21
andrewbrook pushed a commit that referenced this pull request Mar 25, 2026
* implement --only for emulators:export

* remove console logs

* changelog entry

* change  to be nullable

* add tests to make sure exporting with POST works

* Update CHANGELOG.md

---------

Co-authored-by: Joe Hanley <joehanley@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

firebase emulators:export doesn't support / warn about --only

3 participants