From 61a9e58bf31690c675cde2d9150ea8f60b09b4f4 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 18 Feb 2026 16:27:08 -0500 Subject: [PATCH 1/2] checkpoint --- dev/src/pipelines/pipelines.ts | 18 ++++++++++++++++++ dev/src/pipelines/stage.ts | 10 +++++++++- dev/system-test/pipeline.ts | 20 ++++++++++++++++++++ types/firestore.d.ts | 3 --- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/dev/src/pipelines/pipelines.ts b/dev/src/pipelines/pipelines.ts index 060b675b5..3e41ddd8c 100644 --- a/dev/src/pipelines/pipelines.ts +++ b/dev/src/pipelines/pipelines.ts @@ -1507,6 +1507,13 @@ export class Pipeline implements firestore.Pipelines.Pipeline { * @beta * Performs a delete operation on documents from previous stages. * + * @example + * ```typescript + * // Deletes all documents in the "books" collection. + * firestore.pipeline().collection("books") + * .delete(); + * ``` + * * @return A new {@code Pipeline} object with this stage appended to the stage list. */ delete(): Pipeline; @@ -1514,6 +1521,8 @@ export class Pipeline implements firestore.Pipelines.Pipeline { * @beta * Performs a delete operation on documents from previous stages. * + * TODO(dlarocque): Verify we want this function. + * * @param collectionNameOrRef - The collection to delete from. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @@ -1522,6 +1531,15 @@ export class Pipeline implements firestore.Pipelines.Pipeline { * @beta * Performs a delete operation on documents from previous stages. * + * @example + * ```typescript + * // Deletes all documents in the books collection and returns their IDs. + * firestore.pipeline().collection("books") + * .delete({ + * returns: "DOCUMENT_ID", + * }); + * ``` + * * @param options - The {@code DeleteStageOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ diff --git a/dev/src/pipelines/stage.ts b/dev/src/pipelines/stage.ts index 93d5bd439..76b38fcff 100644 --- a/dev/src/pipelines/stage.ts +++ b/dev/src/pipelines/stage.ts @@ -678,11 +678,13 @@ export class RawStage implements Stage { } } +/** + * Delete stage. + */ export class DeleteStage implements Stage { name = 'delete'; readonly optionsUtil = new OptionsUtil({ returns: {serverName: 'returns'}, - transactional: {serverName: 'transactional'}, }); constructor( @@ -708,6 +710,9 @@ export class DeleteStage implements Stage { } } +/** + * Upsert stage. + */ export class UpsertStage implements Stage { name = 'upsert'; readonly optionsUtil = new OptionsUtil({ @@ -740,6 +745,9 @@ export class UpsertStage implements Stage { } } +/** + * Insert stage. + */ export class InsertStage implements Stage { name = 'insert'; readonly optionsUtil = new OptionsUtil({ diff --git a/dev/system-test/pipeline.ts b/dev/system-test/pipeline.ts index b385fda1c..225ffb370 100644 --- a/dev/system-test/pipeline.ts +++ b/dev/system-test/pipeline.ts @@ -328,6 +328,26 @@ describe.skipClassic('Pipeline class', () => { expect(docSnap.exists).to.be.false; }); + it('can execute delete stage with returns document id', async () => { + const docRef = randomCol.doc('testDelete'); + await docRef.set({foo: 'bar'}); + + const ppl = firestore + .pipeline() + .collection(randomCol.path) + .where(equal(field('__name__'), 'testDelete')) + .delete({ + returns: 'DOCUMENT_ID', + }); + + const res = await ppl.execute(); + expect(res.results.length).to.equal(0); + + // verify document was deleted + const docSnap = await docRef.get(); + expect(docSnap.exists).to.be.false; + }); + it('can execute upsert stage', async () => { const docRef = randomCol.doc('testUpsert'); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 3190b7515..5b17485ff 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -10700,7 +10700,6 @@ declare namespace FirebaseFirestore { */ export type DeleteStageOptions = StageOptions & { returns?: DeleteReturn; - transactional?: boolean; }; /** @@ -10723,7 +10722,6 @@ declare namespace FirebaseFirestore { returns?: UpsertReturn; conflict_resolution?: ConflictResolution; transformations?: Record; - transactional?: boolean; }; /** @@ -10739,7 +10737,6 @@ declare namespace FirebaseFirestore { export type InsertStageOptions = StageOptions & { returns?: InsertReturn; transformations?: Record; - transactional?: boolean; }; /** * @beta From 7c8bf839779573d6eea904e83b6e84e4fb0633d0 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 3 Mar 2026 16:19:23 -0500 Subject: [PATCH 2/2] delete tests, update stage, remove options --- dev/src/pipelines/pipelines.ts | 45 +++++++++++++++++++++++++++++++ dev/src/pipelines/stage.ts | 34 +++++++++++++++++++++++ dev/system-test/pipeline.ts | 49 ++++++++++++++++++++++------------ types/firestore.d.ts | 35 ++++++++++++++---------- 4 files changed, 132 insertions(+), 31 deletions(-) diff --git a/dev/src/pipelines/pipelines.ts b/dev/src/pipelines/pipelines.ts index 3e41ddd8c..8eafec8bd 100644 --- a/dev/src/pipelines/pipelines.ts +++ b/dev/src/pipelines/pipelines.ts @@ -98,6 +98,7 @@ import { InternalDocumentsStageOptions, InternalCollectionGroupStageOptions, InternalCollectionStageOptions, + UpdateStage, } from './stage'; import {StructuredPipeline} from './structured-pipeline'; import Selectable = FirebaseFirestore.Pipelines.Selectable; @@ -1565,6 +1566,50 @@ export class Pipeline implements firestore.Pipelines.Pipeline { return this._addStage(new DeleteStage(target, options)); } + /** + * @beta + * Performs an update operation using documents from previous stages. + * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + update(): Pipeline; + /** + * @beta + * Performs an update operation using documents from previous stages. + * + * @param collectionNameOrRef - The collection to update. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + update(collectionNameOrRef: string | firestore.CollectionReference): Pipeline; + /** + * @beta + * Performs an update operation using documents from previous stages. + * + * @param options - The {@code UpdateStageOptions} to apply to the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + update(options: firestore.Pipelines.UpdateStageOptions): Pipeline; + update( + optionsOrCollection?: + | string + | firestore.CollectionReference + | firestore.Pipelines.UpdateStageOptions, + ): Pipeline { + let target = undefined; + if (typeof optionsOrCollection === 'string') { + target = this.db.collection(optionsOrCollection); + } else if (isCollectionReference(optionsOrCollection)) { + target = optionsOrCollection; + } + const options = ( + !isCollectionReference(optionsOrCollection) && + typeof optionsOrCollection !== 'string' + ? optionsOrCollection + : undefined + ) as firestore.Pipelines.UpdateStageOptions | undefined; + return this._addStage(new UpdateStage(target, options)); + } + /** * @beta * Performs an upsert operation using documents from previous stages. diff --git a/dev/src/pipelines/stage.ts b/dev/src/pipelines/stage.ts index 76b38fcff..e3917fac9 100644 --- a/dev/src/pipelines/stage.ts +++ b/dev/src/pipelines/stage.ts @@ -709,6 +709,40 @@ export class DeleteStage implements Stage { }; } } +/** + * Update stage. + */ +export class UpdateStage implements Stage { + name = 'update'; + readonly optionsUtil = new OptionsUtil({ + returns: {serverName: 'returns'}, + conflict_resolution: {serverName: 'conflict_resolution'}, + transformations: {serverName: 'transformations'}, + transactional: {serverName: 'transactional'}, + }); + + constructor( + private target?: firestore.CollectionReference, + private rawOptions?: firestore.Pipelines.UpdateStageOptions, + ) {} + + _toProto(serializer: Serializer): api.Pipeline.IStage { + const args: api.IValue[] = []; + if (this.target) { + args.push({referenceValue: this.target.path}); + } + + return { + name: this.name, + args, + options: this.optionsUtil.getOptionsProto( + serializer, + {}, + this.rawOptions, + ), + }; + } +} /** * Upsert stage. diff --git a/dev/system-test/pipeline.ts b/dev/system-test/pipeline.ts index 225ffb370..aedc7c9a0 100644 --- a/dev/system-test/pipeline.ts +++ b/dev/system-test/pipeline.ts @@ -314,42 +314,57 @@ describe.skipClassic('Pipeline class', () => { const docRef = randomCol.doc('testDelete'); await docRef.set({foo: 'bar'}); - const ppl = firestore + const deletePpl = firestore .pipeline() .collection(randomCol.path) - .where(equal(field('__name__'), 'testDelete')) + .where(equal(field('__name__').documentId(), docRef.id)) .delete(); - const res = await ppl.execute(); - expect(res.results.length).to.equal(0); + const deleteRes = await deletePpl.execute(); + expectResults(deleteRes, {documents_modified: 1}); - // verify document was deleted + // Verify 'testDelete' document was deleted const docSnap = await docRef.get(); expect(docSnap.exists).to.be.false; }); - it('can execute delete stage with returns document id', async () => { + it('can execute delete stage within a transaction', async () => { const docRef = randomCol.doc('testDelete'); await docRef.set({foo: 'bar'}); + await firestore.runTransaction(async transaction => { + const deletePpl = firestore + .pipeline() + .collection(randomCol.path) + .where(equal(field('__name__').documentId(), docRef.id)) + .delete(); + + const deleteRes = await transaction.execute(deletePpl); + expectResults(deleteRes, {documents_modified: 1}); + }); + + // Verify 'testDelete' document was deleted + const docSnap = await docRef.get(); + expect(docSnap.exists).to.be.false; + }); + + it('can execute update stage', async () => { + randomCol.doc('testUpdate'); const ppl = firestore .pipeline() .collection(randomCol.path) .where(equal(field('__name__'), 'testDelete')) - .delete({ - returns: 'DOCUMENT_ID', - }); - - const res = await ppl.execute(); - expect(res.results.length).to.equal(0); + .addFields( + field('__name__').as('id'), + 'upserted_value' as unknown as Pipelines.Selectable, // Hardcoded values inside addFields need specific treatment or aren't supported + ) + .update(randomCol.path); - // verify document was deleted - const docSnap = await docRef.get(); - expect(docSnap.exists).to.be.false; + await ppl.execute(); }); it('can execute upsert stage', async () => { - const docRef = randomCol.doc('testUpsert'); + randomCol.doc('testUpsert'); const ppl = firestore .pipeline() @@ -366,7 +381,7 @@ describe.skipClassic('Pipeline class', () => { }); it('can execute insert stage', async () => { - const docRef = randomCol.doc('testInsert'); + randomCol.doc('testInsert'); const ppl = firestore .pipeline() diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 5b17485ff..ce4962a7b 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -10689,33 +10689,43 @@ declare namespace FirebaseFirestore { }; /** - * @beta + * @internal * Defines the possible return types of a DeleteStage. */ export type DeleteReturn = 'EMPTY' | 'DOCUMENT_ID'; /** * @beta - * Options defining how a DeleteStage is evaluated. + * Options defining how a DeleteStage is evaluated. This is currently a placeholder. */ - export type DeleteStageOptions = StageOptions & { - returns?: DeleteReturn; - }; + export type DeleteStageOptions = StageOptions & {}; /** - * @beta + * @internal + * Defines the possible return types of an UpdateStage. + */ + export type UpdateReturn = 'EMPTY' | 'DOCUMENT_ID'; + + /** + * @internal + * Options defining how an UpdateStage is evaluated. This is currently a placeholder. + */ + export type UpdateStageOptions = StageOptions & {}; + + /** + * @internal * Defines the possible return types of an UpsertStage. */ export type UpsertReturn = 'EMPTY' | 'DOCUMENT_ID'; /** - * @beta + * @internal * Defines the conflict resolution strategy for an UpsertStage. */ export type ConflictResolution = 'OVERWRITE' | 'MERGE' | 'FAIL' | 'KEEP'; /** - * @beta + * @internal * Options defining how an UpsertStage is evaluated. */ export type UpsertStageOptions = StageOptions & { @@ -10725,19 +10735,16 @@ declare namespace FirebaseFirestore { }; /** - * @beta + * @internal * Defines the possible return types of an InsertStage. */ export type InsertReturn = 'EMPTY' | 'DOCUMENT_ID'; /** * @beta - * Options defining how an InsertStage is evaluated. + * Options defining how an InsertStage is evaluated. This is currently a placeholder. */ - export type InsertStageOptions = StageOptions & { - returns?: InsertReturn; - transformations?: Record; - }; + export type InsertStageOptions = StageOptions & {}; /** * @beta * Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}.