-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fixing some issues around handling multiple firestore DBs #9770
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| - Added support for emulating multiple Firestore databases at once (#9742). | ||
| - Fixed an issue causing errors when multiple Firestore databases were configured in `firebase.json` (#8114) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,7 +73,7 @@ | |
|
|
||
| /** | ||
| * Exports emulator data on clean exit (SIGINT or process end) | ||
| * @param options | ||
|
Check warning on line 76 in src/emulator/controller.ts
|
||
| */ | ||
| export async function exportOnExit(options: Options): Promise<void> { | ||
| // Note: options.exportOnExit is coerced to a string before this point in commandUtils.ts#setExportOnExitOptions | ||
|
|
@@ -86,7 +86,7 @@ | |
| ); | ||
| await exportEmulatorData(exportOnExitDir, options, /* initiatedBy= */ "exit"); | ||
| } catch (e: unknown) { | ||
| utils.logWarning(`${e}`); | ||
| utils.logWarning(`Automatic export to "${exportOnExitDir}" failed, going to exit now...`); | ||
| } | ||
| } | ||
|
|
@@ -94,10 +94,10 @@ | |
|
|
||
| /** | ||
| * Hook to do things when we're exiting cleanly (this does not include errors). Will be skipped on a second SIGINT | ||
| * @param options | ||
|
Check warning on line 97 in src/emulator/controller.ts
|
||
| */ | ||
| export async function onExit(options: any) { | ||
|
Check warning on line 99 in src/emulator/controller.ts
|
||
| await exportOnExit(options); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -116,9 +116,9 @@ | |
|
|
||
| /** | ||
| * Filters a list of emulators to only those specified in the config | ||
| * @param options | ||
|
Check warning on line 119 in src/emulator/controller.ts
|
||
| */ | ||
| export function filterEmulatorTargets(options: { only: string; config: any }): Emulators[] { | ||
| let targets = [...ALL_SERVICE_EMULATORS]; | ||
| targets.push(Emulators.EXTENSIONS); | ||
| targets = targets.filter((e) => { | ||
|
|
@@ -671,10 +671,8 @@ | |
| } | ||
|
|
||
| const config = options.config; | ||
| // emulator does not support multiple databases yet | ||
| // TODO(VicVer09): b/269787702 | ||
| let rulesLocalPath; | ||
| let rulesFileFound; | ||
| const rules: { database: string; rules: string }[] = []; | ||
| const firestoreConfigs: fsConfig.ParsedFirestoreConfig[] = fsConfig.getFirestoreConfig( | ||
| projectId, | ||
| options, | ||
|
|
@@ -685,36 +683,37 @@ | |
| "firestore", | ||
| `Cloud Firestore config does not exist in firebase.json.`, | ||
| ); | ||
| } else if (firestoreConfigs.length !== 1) { | ||
| firestoreLogger.logLabeled( | ||
| "WARN", | ||
| "firestore", | ||
| `Cloud Firestore Emulator does not support multiple databases yet.`, | ||
| ); | ||
| } else if (firestoreConfigs[0].rules) { | ||
| rulesLocalPath = firestoreConfigs[0].rules; | ||
| } | ||
| if (rulesLocalPath) { | ||
| const rules: string = config.path(rulesLocalPath); | ||
| rulesFileFound = fs.existsSync(rules); | ||
| if (rulesFileFound) { | ||
| args.rules = rules; | ||
| } else { | ||
| firestoreLogger.logLabeled( | ||
| "WARN", | ||
| "firestore", | ||
| `Cloud Firestore rules file ${clc.bold(rules)} specified in firebase.json does not exist.`, | ||
| ); | ||
| } | ||
| } else { | ||
| for (const firestoreConfig of firestoreConfigs) { | ||
| if (firestoreConfig.rules) { | ||
| const rulesLocalPath = firestoreConfig.rules; | ||
| const rulesAbsolutePath = config.path(rulesLocalPath); | ||
| rulesFileFound = fs.existsSync(rulesAbsolutePath); | ||
| if (rulesFileFound) { | ||
| rules.push({ database: firestoreConfig.database, rules: rulesAbsolutePath }); | ||
| } else { | ||
| firestoreLogger.logLabeled( | ||
| "WARN", | ||
| "firestore", | ||
| `Cloud Firestore rules file ${clc.bold( | ||
| rulesAbsolutePath, | ||
| )} specified in firebase.json does not exist.`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| args.rules = rules; | ||
|
|
||
| if (rules.length === 0) { | ||
| firestoreLogger.logLabeled( | ||
| "WARN", | ||
| "firestore", | ||
| "Did not find a Cloud Firestore rules file specified in a firebase.json config file.", | ||
| ); | ||
| } | ||
|
|
||
| if (!rulesFileFound) { | ||
| if (rules.length === 0) { | ||
| firestoreLogger.logLabeled( | ||
| "WARN", | ||
| "firestore", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ export interface FirestoreEmulatorArgs { | |
| host?: string; | ||
| websocket_port?: number; | ||
| project_id?: string; | ||
| rules?: string; | ||
| rules?: { database: string; rules: string }[]; | ||
| functions_emulator?: string; | ||
| auto_download?: boolean; | ||
| seed_from_export?: string; | ||
|
|
@@ -42,28 +42,33 @@ export class FirestoreEmulator implements EmulatorInstance { | |
| } | ||
|
|
||
| if (this.args.rules && this.args.project_id) { | ||
| const rulesPath = this.args.rules; | ||
| this.rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true }); | ||
| this.rulesWatcher.on("change", async () => { | ||
| // There have been some race conditions reported (on Windows) where reading the | ||
| // file too quickly after the watcher fires results in an empty file being read. | ||
| // Adding a small delay prevents that at very little cost. | ||
| await new Promise((res) => setTimeout(res, 5)); | ||
|
|
||
| utils.logLabeledBullet("firestore", "Change detected, updating rules..."); | ||
| const newContent = fs.readFileSync(rulesPath, "utf8").toString(); | ||
| const issues = await this.updateRules(newContent); | ||
| if (issues) { | ||
| for (const issue of issues) { | ||
| utils.logWarning(this.prettyPrintRulesIssue(rulesPath, issue)); | ||
| for (const c of this.args.rules) { | ||
| const rulesPath = c.rules; | ||
| this.rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true }); | ||
| this.rulesWatcher.on("change", async () => { | ||
| // There have been some race conditions reported (on Windows) where reading the | ||
| // file too quickly after the watcher fires results in an empty file being read. | ||
| // Adding a small delay prevents that at very little cost. | ||
| await new Promise((res) => setTimeout(res, 5)); | ||
|
|
||
| utils.logLabeledBullet( | ||
| "firestore", | ||
| `Change detected, updating rules for ${c.database}...`, | ||
| ); | ||
| const newContent = fs.readFileSync(rulesPath, "utf8").toString(); | ||
| const issues = await this.updateRules(c.database, newContent); | ||
| if (issues) { | ||
| for (const issue of issues) { | ||
| utils.logWarning(this.prettyPrintRulesIssue(rulesPath, issue)); | ||
| } | ||
| } | ||
| } | ||
| if (issues.some((issue) => issue.severity === Severity.ERROR)) { | ||
| utils.logWarning("Failed to update rules"); | ||
| } else { | ||
| utils.logLabeledSuccess("firestore", "Rules updated."); | ||
| } | ||
| }); | ||
| if (issues.some((issue) => issue.severity === Severity.ERROR)) { | ||
| utils.logWarning("Failed to update rules"); | ||
| } else { | ||
| utils.logLabeledSuccess("firestore", "Rules updated."); | ||
| } | ||
| }); | ||
| } | ||
|
Comment on lines
+45
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The const rulesWatchers: chokidar.FSWatcher[] = [];
for (const c of this.args.rules) {
const rulesPath = c.rules;
const watcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true });
watcher.on("change", async () => {
// There have been some race conditions reported (on Windows) where reading the
// file too quickly after the watcher fires results in an empty file being read.
// Adding a small delay prevents that at very little cost.
await new Promise((res) => setTimeout(res, 5));
utils.logLabeledBullet(
"firestore",
`Change detected, updating rules for ${c.database}...`
);
const newContent = fs.readFileSync(rulesPath, "utf8").toString();
const issues = await this.updateRules(c.database, newContent);
if (issues) {
for (const issue of issues) {
utils.logWarning(this.prettyPrintRulesIssue(rulesPath, issue));
}
}
if (issues.some((issue) => issue.severity === Severity.ERROR)) {
utils.logWarning("Failed to update rules");
} else {
utils.logLabeledSuccess("firestore", "Rules updated.");
}
});
rulesWatchers.push(watcher);
}
this.rulesWatcher = rulesWatchers; // Update the type of rulesWatcher to be FSWatcher[] |
||
| } | ||
|
|
||
| return downloadableEmulators.start(Emulators.FIRESTORE, this.args); | ||
|
|
@@ -101,7 +106,7 @@ export class FirestoreEmulator implements EmulatorInstance { | |
| return Emulators.FIRESTORE; | ||
| } | ||
|
|
||
| private async updateRules(content: string): Promise<Issue[]> { | ||
| private async updateRules(databaseId: string, content: string): Promise<Issue[]> { | ||
| const projectId = this.args.project_id; | ||
|
|
||
| const body = { | ||
|
|
@@ -118,7 +123,7 @@ export class FirestoreEmulator implements EmulatorInstance { | |
| }; | ||
|
|
||
| const res = await EmulatorRegistry.client(Emulators.FIRESTORE).put<any, { issues?: Issue[] }>( | ||
| `/emulator/v1/projects/${projectId}:securityRules`, | ||
| `/emulator/v1/projects/${projectId}/databases/${databaseId}:securityRules`, | ||
| body, | ||
| ); | ||
| if (res.body && Array.isArray(res.body.issues)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { expect } from "chai"; | ||
| import { getFirestoreConfig } from "./fsConfig"; | ||
|
|
||
| describe("getFirestoreConfig", () => { | ||
| it("should return all configs when firestore:indexes is specified in only", () => { | ||
| const options: any = { | ||
| config: { | ||
| src: { | ||
| firestore: [ | ||
| { database: "(default)", rules: "firestore.rules" }, | ||
| { database: "second", rules: "firestore.second.rules" }, | ||
| ], | ||
| }, | ||
| }, | ||
| rc: { | ||
| requireTarget: () => { | ||
| return; | ||
| }, | ||
| target: () => [], | ||
| }, | ||
| only: "firestore:indexes", | ||
| }; | ||
|
|
||
| const result = getFirestoreConfig("project", options); | ||
| expect(result).to.have.length(2); | ||
| }); | ||
|
|
||
| it("should return all configs when firestore:rules is specified in only", () => { | ||
| const options: any = { | ||
| config: { | ||
| src: { | ||
| firestore: [ | ||
| { database: "(default)", rules: "firestore.rules" }, | ||
| { database: "second", rules: "firestore.second.rules" }, | ||
| ], | ||
| }, | ||
| }, | ||
| rc: { | ||
| requireTarget: () => { | ||
| return; | ||
| }, | ||
| target: () => [], | ||
| }, | ||
| only: "firestore:rules", | ||
| }; | ||
|
|
||
| const result = getFirestoreConfig("project", options); | ||
| expect(result).to.have.length(2); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the Firestore emulator binary accepts an array of objects for rules(I could be wrong though, I can't find any docs on how to use the binary).
Running the emulator with the code below(from what I can tell, this is what we do with the CLI):
minimal firestore emulator spawn code
Will raise an error
java.io.FileNotFoundException: [object Object],[object Object] (No such file or directory)error output
Changing the code the use a string for
rulesmakes the process run againminimal firestore spawn code
outputs
log output