Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.
Closed
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
63 changes: 63 additions & 0 deletions dev/src/pipelines/pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import {
InternalDocumentsStageOptions,
InternalCollectionGroupStageOptions,
InternalCollectionStageOptions,
UpdateStage,
} from './stage';
import {StructuredPipeline} from './structured-pipeline';
import Selectable = FirebaseFirestore.Pipelines.Selectable;
Expand Down Expand Up @@ -1507,13 +1508,22 @@ 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;
/**
* @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.
*/
Expand All @@ -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.
*/
Expand All @@ -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.
Expand Down
44 changes: 43 additions & 1 deletion dev/src/pipelines/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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({
Expand Down Expand Up @@ -740,6 +779,9 @@ export class UpsertStage implements Stage {
}
}

/**
* Insert stage.
*/
export class InsertStage implements Stage {
name = 'insert';
readonly optionsUtil = new OptionsUtil({
Expand Down
49 changes: 42 additions & 7 deletions dev/system-test/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
38 changes: 21 additions & 17 deletions types/firestore.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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<string, unknown>;
transactional?: boolean;
};
export type InsertStageOptions = StageOptions & {};
/**
* @beta
* Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}.
Expand Down
Loading