Skip to content
Open
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
119 changes: 119 additions & 0 deletions packages/firestore/src/lite-api/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
Sort,
Stage,
Union,
Delete,
Upsert,
Insert,
Unnest,
Where
} from './stage';
Expand All @@ -82,11 +85,15 @@
SortStageOptions,
StageOptions,
UnionStageOptions,
DeleteStageOptions,
UpsertStageOptions,
InsertStageOptions,
UnnestStageOptions,
WhereStageOptions
} from './stage_options';
import { UserDataReader, UserDataSource } from './user_data_reader';
import { AbstractUserDataWriter } from './user_data_writer';
import { CollectionReference } from './reference';

Check failure on line 96 in packages/firestore/src/lite-api/pipeline.ts

View workflow job for this annotation

GitHub Actions / Lint

`./reference` import should occur before import of `./stage`

/**
* @beta
Expand Down Expand Up @@ -1500,6 +1507,118 @@
): Pipeline {
return new Pipeline(db, userDataReader, userDataWriter, stages);
}
/**
* @beta
* Deletes the documents resulting from the pipeline.
*
* @example
* ```typescript
* firestore.pipeline().collection('books').where(field('rating').lt(2)).delete();
* ```
*
* @param options - Optional parameters for the stage.
* @returns A new Pipeline object with this stage appended to the stage list.
*/
delete(): Pipeline;
delete(options: DeleteStageOptions): Pipeline;
delete(options?: DeleteStageOptions): Pipeline {
const stage = new Delete(options || {});

// User data must be read in the context of the API method to
// provide contextual errors
const parseContext = this.userDataReader.createContext(
UserDataSource.Argument,
'delete'
);
stage._readUserData(parseContext);

return this._addStage(stage);
}

/**
* @beta
* Upserts the documents resulting from the pipeline.
*
* @example
* ```typescript
* firestore.pipeline().collection('new_books').upsert('books');
* ```
*
* @param collectionOrOptions - The target collection or options for the stage.
* @returns A new Pipeline object with this stage appended to the stage list.
*/
upsert(): Pipeline;
upsert(collection: string | CollectionReference): Pipeline;
upsert(options: UpsertStageOptions): Pipeline;
upsert(
collectionOrOptions?: string | CollectionReference | UpsertStageOptions
): Pipeline {
let options: UpsertStageOptions;
if (
typeof collectionOrOptions === 'string' ||
(typeof collectionOrOptions === 'object' &&
'type' in collectionOrOptions &&
collectionOrOptions.type === 'collection')
Comment on lines +1558 to +1561
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type check for CollectionReference is a bit brittle and is duplicated in the insert method below. It can be simplified and made more robust by using the isCollectionReference type guard, which is exported from ./reference.

For example, if collectionOrOptions is null, typeof collectionOrOptions is 'object', which would cause a TypeError on the 'type' in collectionOrOptions check.

You'll need to import isCollectionReference from ./reference to use it.

      typeof collectionOrOptions === 'string' ||
      isCollectionReference(collectionOrOptions)

) {
options = { collection: collectionOrOptions as string | CollectionReference };
} else {
options = (collectionOrOptions as UpsertStageOptions) || {};
}

const stage = new Upsert(options);

// User data must be read in the context of the API method to
// provide contextual errors
const parseContext = this.userDataReader.createContext(
UserDataSource.Argument,
'upsert'
);
stage._readUserData(parseContext);

return this._addStage(stage);
}

/**
* @beta
* Inserts the documents resulting from the pipeline.
*
* @example
* ```typescript
* firestore.pipeline().collection('new_books').insert('books');
* ```
*
* @param collectionOrOptions - The target collection or options for the stage.
* @returns A new Pipeline object with this stage appended to the stage list.
*/
insert(collection: string | CollectionReference): Pipeline;
insert(options: InsertStageOptions): Pipeline;
insert(
collectionOrOptions: string | CollectionReference | InsertStageOptions
): Pipeline {
let options: InsertStageOptions;
if (
typeof collectionOrOptions === 'string' ||
(typeof collectionOrOptions === 'object' &&
'type' in collectionOrOptions &&
collectionOrOptions.type === 'collection')
) {
options = { collection: collectionOrOptions as string | CollectionReference };
} else {
options = collectionOrOptions as InsertStageOptions;
}

const stage = new Insert(options);

// User data must be read in the context of the API method to
// provide contextual errors
const parseContext = this.userDataReader.createContext(
UserDataSource.Argument,
'insert'
);
stage._readUserData(parseContext);

return this._addStage(stage);
}
}

export function isPipeline(val: unknown): val is Pipeline {
Expand Down
128 changes: 127 additions & 1 deletion packages/firestore/src/lite-api/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
Expression,
Field,
field,
Ordering
Ordering,
Selectable

Check failure on line 42 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

'Selectable' is defined but never used

Check failure on line 42 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

'Selectable' is defined but never used. Allowed unused vars must match /^_/u
} from './expressions';
import { Pipeline } from './pipeline';
import { StageOptions } from './stage_options';
import { isUserData, UserData } from './user_data_reader';

Check failure on line 46 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import { selectablesToMap } from '../util/pipeline_util';

Check failure on line 47 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

'selectablesToMap' is defined but never used

Check failure on line 47 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

'selectablesToMap' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 47 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

`../util/pipeline_util` import should occur before import of `./expressions`

/**
* @beta
Expand Down Expand Up @@ -762,3 +764,127 @@
}
return expressionMap;
}

/**
* @beta
*/
export class Delete extends Stage {
get _name(): string {
return 'delete';
}

get _optionsUtil(): OptionsUtil {
return new OptionsUtil({
transactional: { serverName: 'transactional' },
returns: { serverName: 'returns' }
});
}

constructor(options: StageOptions) {

Check failure on line 783 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

Useless constructor
super(options);
}

/**
* @internal
* @private
*/
_toProto(serializer: JsonProtoSerializer): ProtoStage {
return {
...super._toProto(serializer),
args: []
};
}

_readUserData(context: ParseContext): void {
super._readUserData(context);
}
}

/**
* @beta
*/
export class Upsert extends Stage {
get _name(): string {
return 'upsert';
}

get _optionsUtil(): OptionsUtil {
return new OptionsUtil({
transactional: { serverName: 'transactional' },
returns: { serverName: 'returns' },
conflictResolution: { serverName: 'conflict_resolution' }
});
}

constructor(options: StageOptions) {
super(options);

if ('collection' in this.knownOptions && typeof this.knownOptions.collection === 'string') {
const collection = this.knownOptions.collection;
this.knownOptions.collection = collection.startsWith('/') ? collection : '/' + collection;
}
}

/**
* @internal
* @private
*/
_toProto(serializer: JsonProtoSerializer): ProtoStage {
let args: any[] = [];

Check failure on line 833 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check failure on line 833 in packages/firestore/src/lite-api/stage.ts

View workflow job for this annotation

GitHub Actions / Lint

'args' is never reassigned. Use 'const' instead
if (this.knownOptions.collection) {
args.push({ referenceValue: this.knownOptions.collection });
}
return {
...super._toProto(serializer),
args
};
}

_readUserData(context: ParseContext): void {
super._readUserData(context);
}
}
Comment on lines +806 to +846
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Upsert and Insert classes share identical logic for normalizing the collection path in their constructors and for serializing the collection option in their _toProto methods. This duplication can be avoided by extracting the common logic into a shared base class (e.g., CollectionMutationStage) that both Upsert and Insert can extend. This would improve maintainability and reduce code redundancy.


/**
* @beta
*/
export class Insert extends Stage {
get _name(): string {
return 'insert';
}

get _optionsUtil(): OptionsUtil {
return new OptionsUtil({
transactional: { serverName: 'transactional' },
returns: { serverName: 'returns' }
});
}

constructor(options: StageOptions) {
super(options);

if ('collection' in this.knownOptions && typeof this.knownOptions.collection === 'string') {
const collection = this.knownOptions.collection;
this.knownOptions.collection = collection.startsWith('/') ? collection : '/' + collection;
}
}

/**
* @internal
* @private
*/
_toProto(serializer: JsonProtoSerializer): ProtoStage {
let args: any[] = [];
if (this.knownOptions.collection) {
args.push({ referenceValue: this.knownOptions.collection });
}
return {
...super._toProto(serializer),
args
};
}

_readUserData(context: ParseContext): void {
super._readUserData(context);
}
}
73 changes: 73 additions & 0 deletions packages/firestore/src/lite-api/stage_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,76 @@ export type SortStageOptions = StageOptions & {
*/
orderings: Ordering[];
};

/**
* @beta
* Options defining how a WriteStage is evaluated.
*/
export type WriteStageOptions = StageOptions & {
/**
* @beta
* Whether the write should be performed in a transaction.
*/
transactional?: boolean;
};

/**
* @beta
* Options defining how a DeleteStage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(delete:1)}.
*/
export type DeleteStageOptions = WriteStageOptions & {
/**
* @beta
* Specifies what to return from the delete operation.
*/
returns?: 'empty' | 'document_id';
};

/**
* @beta
* Options defining how an UpsertStage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(upsert:1)}.
*/
export type UpsertStageOptions = WriteStageOptions & {
/**
* @beta
* Name or reference to the collection where documents will be upserted.
*/
collection?: string | CollectionReference;
/**
* @beta
* Specifies what to return from the upsert operation.
*/
returns?: 'empty' | 'document_id';
/**
* @beta
* Specifies the conflict resolution strategy.
*/
conflictResolution?: 'overwrite' | 'merge' | 'fail' | 'keep';
/**
* @beta
* Expressions to apply during the upsert.
*/
transformations?: Selectable[];
};

/**
* @beta
* Options defining how an InsertStage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(insert:1)}.
*/
export type InsertStageOptions = WriteStageOptions & {
/**
* @beta
* Name or reference to the collection where documents will be inserted.
*/
collection: string | CollectionReference;
/**
* @beta
* Specifies what to return from the insert operation.
*/
returns?: 'empty' | 'document_id';
/**
* @beta
* Expressions to apply during the insert.
*/
transformations?: Selectable[];
};
Loading
Loading