diff --git a/dev/src/pipelines/pipelines.ts b/dev/src/pipelines/pipelines.ts index 060b675b5..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; @@ -1507,6 +1508,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 +1522,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 +1532,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. */ @@ -1547,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 93d5bd439..e3917fac9 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( @@ -707,7 +709,44 @@ 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. + */ export class UpsertStage implements Stage { name = 'upsert'; readonly optionsUtil = new OptionsUtil({ @@ -740,6 +779,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..aedc7c9a0 100644 --- a/dev/system-test/pipeline.ts +++ b/dev/system-test/pipeline.ts @@ -314,22 +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 'testDelete' document was deleted + const docSnap = await docRef.get(); + expect(docSnap.exists).to.be.false; + }); + + 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 document was deleted + // 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')) + .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); + + await ppl.execute(); + }); + it('can execute upsert stage', async () => { - const docRef = randomCol.doc('testUpsert'); + randomCol.doc('testUpsert'); const ppl = firestore .pipeline() @@ -346,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 3190b7515..ce4962a7b 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -10689,58 +10689,62 @@ 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; - transactional?: boolean; - }; + 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 & { returns?: UpsertReturn; conflict_resolution?: ConflictResolution; transformations?: Record; - transactional?: boolean; }; /** - * @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; - transactional?: boolean; - }; + export type InsertStageOptions = StageOptions & {}; /** * @beta * Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}.