From 2b0b77cd7b956bcf00e1b985616de315fd653bed Mon Sep 17 00:00:00 2001 From: Om Singhal Date: Sat, 22 Aug 2026 20:43:00 -0400 Subject: [PATCH] fix(firestore): accept pipeline references from the same client before initialization Passing a DocumentReference or CollectionReference to pipeline().documents() or pipeline().collection() threw "INTERNAL ERROR: Client is not yet ready to issue requests." whenever the Firestore instance had no explicit project ID and had not yet issued a request. PipelineSource._validateReference compared reference.firestore.formattedName with the pipeline's own formattedName, and that getter reads the projectId getter, which throws until initializeIfNeeded() has detected the project. Pipeline construction is synchronous and happens before any request, so a fresh client always hit this path. Passing the same location as a string path worked because that code never reads the project ID. A reference created by the same Firestore instance necessarily targets the same database, so _validateReference now returns early when reference.firestore is the pipeline's own instance and only compares formatted names for references that come from a different instance. The existing cross database check is unchanged. Adds unit tests that build and execute a pipeline from a DocumentReference and from a CollectionReference on a client whose project ID is only detected on the first request, plus tests that references targeting a different database are still rejected. Fixes #9186 --- .../firestore/dev/src/pipelines/pipelines.ts | 9 ++ .../firestore/dev/test/pipelines/pipeline.ts | 116 ++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/handwritten/firestore/dev/src/pipelines/pipelines.ts b/handwritten/firestore/dev/src/pipelines/pipelines.ts index 9342cedce784..f9fa6c04fb2a 100644 --- a/handwritten/firestore/dev/src/pipelines/pipelines.ts +++ b/handwritten/firestore/dev/src/pipelines/pipelines.ts @@ -290,6 +290,15 @@ export class PipelineSource implements firestore.Pipelines.PipelineSource { ); } + // A reference created by this Firestore instance necessarily targets the + // same database, so there is nothing to compare. This also avoids reading + // `formattedName`, which requires the project ID and throws if the client + // has not been initialized yet (the project ID may only be detected when + // the first request is issued). + if (reference.firestore === this.db) { + return true; + } + const refDbId = reference.firestore.formattedName; if (refDbId !== this.db.formattedName) { throw new Error( diff --git a/handwritten/firestore/dev/test/pipelines/pipeline.ts b/handwritten/firestore/dev/test/pipelines/pipeline.ts index fa48b1c49ffb..4d7961fa9162 100644 --- a/handwritten/firestore/dev/test/pipelines/pipeline.ts +++ b/handwritten/firestore/dev/test/pipelines/pipeline.ts @@ -180,3 +180,119 @@ describe('execute(Pipeline|PipelineExecuteOptions)', () => { ); }); }); + +describe('PipelineSource reference validation', () => { + it('accepts a DocumentReference before the project ID is detected', async () => { + const spy = sinon.fake.returns(stream()); + const firestore = await createInstance( + { + getProjectId: () => Promise.resolve('detected-project'), + executePipeline: spy, + }, + {projectId: undefined}, + ); + + // The project ID is only detected when the first request is issued, so + // building a pipeline from a reference must not depend on it. + const pipeline = firestore.pipeline().documents([firestore.doc('foo/bar')]); + + await pipeline.execute(); + + const executePipelineRequest: IExecutePipelineRequest = { + database: 'projects/detected-project/databases/(default)', + structuredPipeline: { + options: {}, + pipeline: { + stages: [ + { + args: [ + { + referenceValue: '/foo/bar', + }, + ], + name: 'documents', + options: {}, + }, + ], + }, + }, + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest, + ); + }); + + it('accepts a CollectionReference before the project ID is detected', async () => { + const spy = sinon.fake.returns(stream()); + const firestore = await createInstance( + { + getProjectId: () => Promise.resolve('detected-project'), + executePipeline: spy, + }, + {projectId: undefined}, + ); + + const pipeline = firestore + .pipeline() + .collection(firestore.collection('foo')); + + await pipeline.execute(); + + const executePipelineRequest: IExecutePipelineRequest = { + database: 'projects/detected-project/databases/(default)', + structuredPipeline: { + options: {}, + pipeline: { + stages: [ + { + args: [ + { + referenceValue: '/foo', + }, + ], + name: 'collection', + options: {}, + }, + ], + }, + }, + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest, + ); + }); + + it('rejects a DocumentReference that targets a different database', async () => { + const firestore = await createInstance(); + const otherFirestore = await createInstance(undefined, { + databaseId: 'other-db', + }); + + expect(() => + firestore.pipeline().documents([otherFirestore.doc('foo/bar')]), + ).to.throw( + 'Invalid DocumentReference. The database name ' + + '("projects/test-project/databases/other-db") of this reference ' + + 'does not match the database name ' + + '("projects/test-project/databases/(default)") of the target ' + + 'database of this Pipeline.', + ); + }); + + it('rejects a CollectionReference that targets a different database', async () => { + const firestore = await createInstance(); + const otherFirestore = await createInstance(undefined, { + databaseId: 'other-db', + }); + + expect(() => + firestore.pipeline().collection(otherFirestore.collection('foo')), + ).to.throw( + 'Invalid CollectionReference. The database name ' + + '("projects/test-project/databases/other-db") of this reference ' + + 'does not match the database name ' + + '("projects/test-project/databases/(default)") of the target ' + + 'database of this Pipeline.', + ); + }); +});