Skip to content

Read the new dataDir for exporting behaviors #8794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: newExports
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scripts/emulator-import-export-tests/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"storage": {
"port": 9199
}
},
"dataDir": "hello"
}
}
152 changes: 126 additions & 26 deletions scripts/emulator-import-export-tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
function readConfig(): FrameworkOptions {
const filename = path.join(__dirname, "firebase.json");
const data = fs.readFileSync(filename, "utf8");
return JSON.parse(data);

Check warning on line 43 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value
}

function logIncludes(msg: string) {
Expand Down Expand Up @@ -90,7 +90,7 @@
await importCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "firestore", "--import", exportPath],
["--only", "firestore", "--import", exportPath, "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand All @@ -113,7 +113,7 @@
await emulatorsCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "database"],
["--only", "database", "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand All @@ -124,7 +124,7 @@

// Write some data to export
const config = readConfig();
const port = config.emulators!.database.port;

Check warning on line 127 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const host = await localhost();
const aApp = admin.initializeApp(
{
Expand Down Expand Up @@ -185,7 +185,7 @@
await importCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "database", "--import", exportPath, "--export-on-exit"],
["--only", "database", "--import", exportPath, "--export-on-exit", "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand Down Expand Up @@ -214,11 +214,11 @@

// Confirm the data exported is as expected
const aPath = path.join(dbExportPath, "namespace-a.json");
const aData = JSON.parse(fs.readFileSync(aPath).toString());

Check warning on line 217 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
expect(aData).to.deep.equal({ ns: "namespace-a" });

const bPath = path.join(dbExportPath, "namespace-b.json");
const bData = JSON.parse(fs.readFileSync(bPath).toString());

Check warning on line 221 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
expect(bData).to.equal(null);
});

Expand All @@ -230,16 +230,21 @@
const project = FIREBASE_PROJECT || "example";
const emulatorsCLI = new CLIProcess("1", __dirname);

await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
});
await emulatorsCLI.start(
"emulators:start",
project,
["--only", "auth", "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
},
);

// Create some accounts to export:
const config = readConfig();
const port = config.emulators!.auth.port;

Check warning on line 247 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
try {
process.env.FIREBASE_AUTH_EMULATOR_HOST = `${await localhost()}:${port}`;
const adminApp = admin.initializeApp(
Expand Down Expand Up @@ -272,7 +277,7 @@

// Confirm the data is exported as expected
const configPath = path.join(exportPath, "auth_export", "config.json");
const configData = JSON.parse(fs.readFileSync(configPath).toString());

Check warning on line 280 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
expect(configData).to.deep.equal({
signIn: {
allowDuplicateEmails: false,
Expand All @@ -283,9 +288,9 @@
});

const accountsPath = path.join(exportPath, "auth_export", "accounts.json");
const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString());

Check warning on line 291 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
expect(accountsData.users).to.have.length(2);

Check warning on line 292 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .users on an `any` value
expect(accountsData.users[0]).to.deep.contain({

Check warning on line 293 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .users on an `any` value
localId: "123",
email: "foo@example.com",
emailVerified: false,
Expand All @@ -298,7 +303,7 @@
},
],
});
expect(accountsData.users[0].passwordHash).to.match(/:password=testing$/);

Check warning on line 306 in scripts/emulator-import-export-tests/tests.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .users on an `any` value
expect(accountsData.users[1]).to.deep.contain({
localId: "456",
email: "bar@example.com",
Expand All @@ -310,7 +315,7 @@
await importCLI.start(
"emulators:start",
project,
["--only", "auth", "--import", exportPath],
["--only", "auth", "--import", exportPath, "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand Down Expand Up @@ -339,12 +344,17 @@
const project = FIREBASE_PROJECT || "example";
const emulatorsCLI = new CLIProcess("1", __dirname);

await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
});
await emulatorsCLI.start(
"emulators:start",
project,
["--only", "auth", "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
},
);

// Create some accounts to export:
const accountCount = 777; // ~120KB data when exported
Expand Down Expand Up @@ -399,7 +409,7 @@
await importCLI.start(
"emulators:start",
project,
["--only", "auth", "--import", exportPath],
["--only", "auth", "--import", exportPath, "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand All @@ -425,12 +435,17 @@
const project = FIREBASE_PROJECT || "example";
const emulatorsCLI = new CLIProcess("1", __dirname);

await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
});
await emulatorsCLI.start(
"emulators:start",
project,
["--only", "auth", "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
}
return data.includes(ALL_EMULATORS_STARTED_LOG);
},
);

// Ask for export (with no users)
const exportCLI = new CLIProcess("2", __dirname);
Expand Down Expand Up @@ -467,7 +482,7 @@
await importCLI.start(
"emulators:start",
project,
["--only", "auth", "--import", exportPath],
["--only", "auth", "--import", exportPath, "--debug"],
(data: unknown) => {
if (typeof data !== "string" && !Buffer.isBuffer(data)) {
throw new Error(`data is not a string or buffer (${typeof data})`);
Expand All @@ -488,7 +503,7 @@
await emulatorsCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage"],
["--only", "storage", "--debug"],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

Expand Down Expand Up @@ -549,7 +564,7 @@
await importCLI.start(
"emulators:start",
FIREBASE_PROJECT,
["--only", "storage", "--import", exportPath],
["--only", "storage", "--import", exportPath, "--debug"],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

Expand All @@ -576,4 +591,89 @@

await importCLI.stop();
});

it("should automatically export to dataDir when specified in firebase.json", async function (this) {
this.timeout(2 * TEST_SETUP_TIMEOUT);
await new Promise((resolve) => setTimeout(resolve, 2000));

const dataDir = path.join(__dirname, "hello"); // This is taken from firebase.json dataDir

const firestoreData = { testCollection: { testDoc: { foo: "bar", baz: "buzz" } } };

// Start emulator - it should export data to dataDir on exit
let emulatorsCLI = new CLIProcess("dataDir-export", __dirname);
await emulatorsCLI.start(
"emulators:start",
"datadir-export-project",
["--only", "firestore", "--debug"],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

// Add data to Firestore
const configForAdmin = readConfig(); // Assuming readConfig() gets Firestore port
const port = configForAdmin.emulators!.firestore.port;
const host = await localhost(); // Make sure localhost is resolved
process.env.FIRESTORE_EMULATOR_HOST = `${host}:${port}`;
const adminApp = admin.initializeApp(
{
projectId: "datadir-export-project",
},
"firestore-dataDir-test-export",
);

const db = adminApp.firestore();
const docRefId = (
await db.collection("testCollection").add(firestoreData.testCollection.testDoc)
).id;

await adminApp.delete();
// Stop the emulator suite - this should trigger export to dataDir
await emulatorsCLI.stop();

// Verify data was exported to dataDir
// Firestore data is stored in a subdirectory named after the project ID, then 'fs_export_output'
// and then a metadata file and the actual data files.
const firestoreExportMetadataPath = path.join(
dataDir,
"firestore_export",
"firestore_export.overall_export_metadata",
);
expect(
fs.existsSync(firestoreExportMetadataPath),
`Firestore export metadata should exist at ${firestoreExportMetadataPath}`,
).to.be.true;

// Start emulator again - it should automatically import from dataDir
emulatorsCLI = new CLIProcess("dataDir-import", __dirname);
await emulatorsCLI.start(
"emulators:start",
"datadir-export-project",
// TODO(christhompsongoogle): Remove the import once imports from dataDir are implemented.
// Also note to self: ensure the import flag supercedes the dataDir property.
["--only", "firestore", "--debug", "--import", dataDir],
logIncludes(ALL_EMULATORS_STARTED_LOG),
);

// Verify data was imported
const adminAppImport = admin.initializeApp(
{
projectId: "datadir-export-project",
},
"firestore-dataDir-test-import",
);

const dbImport = adminAppImport.firestore();
const docSnap = await dbImport.collection("testCollection").doc(docRefId).get();
expect(docSnap.exists, "Document should exist after import from dataDir").to.be.true;
expect(docSnap.data(), "Document data should match after import from dataDir").to.deep.equal(
firestoreData.testCollection.testDoc,
);
await adminAppImport.delete();

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

// Clean up temporary directory
fs.rmSync(dataDir, { recursive: true, force: true });
});
});
32 changes: 20 additions & 12 deletions src/emulator/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,26 @@ const START_LOGGING_EMULATOR = utils.envOverride(
*/
export async function exportOnExit(options: Options): Promise<void> {
// Note: options.exportOnExit is coerced to a string before this point in commandUtils.ts#setExportOnExitOptions
const exportOnExitDir = options.exportOnExit as string;
if (exportOnExitDir) {
try {
utils.logBullet(
`Automatically exporting data using ${FLAG_EXPORT_ON_EXIT_NAME} "${exportOnExitDir}" ` +
"please wait for the export to finish...",
);
await exportEmulatorData(exportOnExitDir, options, /* initiatedBy= */ "exit");
} catch (e: unknown) {
utils.logWarning(`${e}`);
utils.logWarning(`Automatic export to "${exportOnExitDir}" failed, going to exit now...`);
}
let exportOnExitDir: string;
if (options.exportOnExit) {
// CLI flag takes priority over firebase.json value
exportOnExitDir = options.exportOnExit as string;
} else if (options.config.src.emulators?.dataDir) {
exportOnExitDir = options.config.src.emulators.dataDir;
} else {
// If no export directory, skip the export.
return;
}

try {
utils.logBullet(
`Automatically exporting data using ${FLAG_EXPORT_ON_EXIT_NAME} "${exportOnExitDir}" ` +
"please wait for the export to finish...",
);
await exportEmulatorData(exportOnExitDir, options, /* initiatedBy= */ "exit");
} catch (e: unknown) {
utils.logWarning(`${e}`);
utils.logWarning(`Automatic export to "${exportOnExitDir}" failed, going to exit now...`);
}
}

Expand Down
Loading