diff --git a/common/api-review/firestore-lite-pipelines.api.md b/common/api-review/firestore-lite-pipelines.api.md new file mode 100644 index 00000000000..28746295ae5 --- /dev/null +++ b/common/api-review/firestore-lite-pipelines.api.md @@ -0,0 +1,1315 @@ +## API Report File for "@firebase/firestore-lite-pipelines" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app'; + +// @beta +export function add(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function add(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta (undocumented) +export class AddFields implements Stage { + constructor(fields: Map); + // (undocumented) + name: string; +} + +// @beta (undocumented) +export class Aggregate implements Stage { + constructor(accumulators: Map, groups: Map); + // (undocumented) + name: string; +} + +// @beta +export class AggregateFunction { + constructor(name: string, params: Expr[]); + as(name: string): AggregateWithAlias; + // (undocumented) + exprType: ExprType; + } + +// @beta +export class AggregateWithAlias { + constructor(aggregate: AggregateFunction, alias: string); + // (undocumented) + readonly aggregate: AggregateFunction; + // (undocumented) + readonly alias: string; +} + +// @beta +export function and(first: BooleanExpr, second: BooleanExpr, ...more: BooleanExpr[]): BooleanExpr; + +// @beta +export function array(elements: unknown[]): FunctionExpr; + +// @beta +export function arrayConcat(firstArray: Expr, secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + +// @beta +export function arrayConcat(firstArrayField: string, secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + +// @beta +export function arrayContains(array: Expr, element: Expr): FunctionExpr; + +// @beta +export function arrayContains(array: Expr, element: unknown): FunctionExpr; + +// @beta +export function arrayContains(fieldName: string, element: Expr): FunctionExpr; + +// @beta +export function arrayContains(fieldName: string, element: unknown): BooleanExpr; + +// @beta +export function arrayContainsAll(array: Expr, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAll(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAll(array: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function arrayContainsAll(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta +export function arrayContainsAny(array: Expr, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; + +// @beta +export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; + +// @beta +export function arrayLength(array: Expr): FunctionExpr; + +// @beta +export function arrayOffset(arrayField: string, offset: number): FunctionExpr; + +// @beta +export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; + +// @beta +export function arrayOffset(arrayExpression: Expr, offset: number): FunctionExpr; + +// @beta +export function arrayOffset(arrayExpression: Expr, offsetExpr: Expr): FunctionExpr; + +// @beta +export function ascending(expr: Expr): Ordering; + +// @beta +export function ascending(fieldName: string): Ordering; + +// @beta +export function avg(expression: Expr): AggregateFunction; + +// @beta +export function avg(fieldName: string): AggregateFunction; + +// Warning: (ae-forgotten-export) The symbol "Bytes" needs to be exported by the entry point pipelines.d.ts +// +// @beta +export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitAnd(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitAnd(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export function bitLeftShift(field: string, y: number): FunctionExpr; + +// @beta +export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; + +// @beta +export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitNot(field: string): FunctionExpr; + +// @beta +export function bitNot(bitsValueExpression: Expr): FunctionExpr; + +// @beta +export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitOr(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitOr(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export function bitRightShift(field: string, y: number): FunctionExpr; + +// @beta +export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitRightShift(xValue: Expr, y: number): FunctionExpr; + +// @beta +export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitXor(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitXor(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export class BooleanExpr extends FunctionExpr { + countIf(): AggregateFunction; + // (undocumented) + filterable: true; + not(): BooleanExpr; +} + +// @beta +export function byteLength(expr: Expr): FunctionExpr; + +// @beta +export function byteLength(fieldName: string): FunctionExpr; + +// @beta +export function charLength(fieldName: string): FunctionExpr; + +// @beta +export function charLength(stringExpression: Expr): FunctionExpr; + +// @beta (undocumented) +export class CollectionGroupSource implements Stage { + constructor(collectionId: string); + // (undocumented) + name: string; +} + +// @beta (undocumented) +export class CollectionSource implements Stage { + constructor(collectionPath: string); + // (undocumented) + name: string; +} + +// @beta +export function cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): FunctionExpr; + +// @beta +export class Constant extends Expr { + // (undocumented) + readonly exprType: ExprType; + } + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: number): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: string): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: boolean): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: null): Constant; + +// Warning: (ae-forgotten-export) The symbol "GeoPoint" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: GeoPoint): Constant; + +// Warning: (ae-forgotten-export) The symbol "Timestamp" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Timestamp): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Date): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Bytes): Constant; + +// Warning: (ae-forgotten-export) The symbol "DocumentReference" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: DocumentReference): Constant; + +// Warning: (ae-forgotten-export) The symbol "VectorValue" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: VectorValue): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constantVector" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constantVector(value: number[] | VectorValue): Constant; + +// @beta +export function cosineDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function cosineDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function cosineDistance(vectorExpression: Expr, vector: number[] | Expr): FunctionExpr; + +// @beta +export function cosineDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function count(expression: Expr): AggregateFunction; + +// Warning: (ae-incompatible-release-tags) The symbol "count" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta +// +// @public +export function count(fieldName: string): AggregateFunction; + +// @beta +export function countAll(): AggregateFunction; + +// @beta +export function countIf(booleanExpr: BooleanExpr): AggregateFunction; + +// @beta +export function currentContext(): FunctionExpr; + +// @beta (undocumented) +export class DatabaseSource implements Stage { + // (undocumented) + name: string; +} + +// @beta +export function descending(expr: Expr): Ordering; + +// @beta +export function descending(fieldName: string): Ordering; + +// @beta (undocumented) +export class Distinct implements Stage { + constructor(groups: Map); + // (undocumented) + name: string; +} + +// @beta +export function divide(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function divide(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function divide(fieldName: string, expressions: Expr): FunctionExpr; + +// @beta +export function divide(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function documentId(documentPath: string | DocumentReference): FunctionExpr; + +// @beta +export function documentId(documentPathExpr: Expr): FunctionExpr; + +// @beta (undocumented) +export class DocumentsSource implements Stage { + constructor(docPaths: string[]); + // (undocumented) + name: string; + // (undocumented) + static of(refs: Array): DocumentsSource; +} + +// @beta +export function dotProduct(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function dotProduct(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function dotProduct(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function dotProduct(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function endsWith(fieldName: string, suffix: string): BooleanExpr; + +// @beta +export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; + +// @beta +export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; + +// @beta +export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; + +// @beta +export function eq(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function eq(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function eq(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function eq(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function eqAny(expression: Expr, values: Array): BooleanExpr; + +// @beta +export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function eqAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta +export function euclideanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function euclideanDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function euclideanDistance(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function euclideanDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @public +export function execute(pipeline: Pipeline): Promise; + +// @beta +export function exists(value: Expr): BooleanExpr; + +// @beta +export function exists(fieldName: string): BooleanExpr; + +// @beta +export abstract class Expr { + add(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayConcat(secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayContains(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContains(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayOffset(offset: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayOffset(offsetExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + as(name: string): ExprWithAlias; + /* Excluded from this release type: _readUserData */ + ascending(): Ordering; + /* Excluded from this release type: _readUserData */ + avg(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + bitAnd(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitAnd(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitLeftShift(y: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitLeftShift(numberExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitNot(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitOr(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitOr(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitRightShift(y: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitRightShift(numberExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitXor(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitXor(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + byteLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + charLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + cosineDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + cosineDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + count(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + descending(): Ordering; + /* Excluded from this release type: _readUserData */ + divide(other: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + divide(other: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + documentId(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + dotProduct(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + dotProduct(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eq(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eq(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eqAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eqAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + exists(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + // (undocumented) + abstract readonly exprType: ExprType; + /* Excluded from this release type: _readUserData */ + gt(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gt(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gte(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gte(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + ifError(catchExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + ifError(catchValue: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + isAbsent(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isError(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNan(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNotNan(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNotNull(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNull(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + like(pattern: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + like(pattern: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + logicalMaximum(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + logicalMinimum(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + lt(experession: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lt(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lte(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lte(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + manhattanDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + manhattanDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapGet(subfield: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapMerge(secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapRemove(key: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapRemove(keyExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + maximum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + minimum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + mod(expression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mod(value: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + multiply(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + neq(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + neq(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + notEqAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + notEqAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + replaceAll(find: string, replace: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceAll(find: Expr, replace: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceFirst(find: string, replace: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceFirst(find: Expr, replace: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + reverse(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + strConcat(secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + strContains(substring: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + strContains(expr: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + substr(position: number, length?: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + substr(position: Expr, length?: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + subtract(other: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + subtract(other: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + sum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: Expr, amount: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampSub(unit: Expr, amount: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampSub(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixMicros(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixMillis(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixSeconds(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + toLower(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + toUpper(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + trim(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixMicrosToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixMillisToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixSecondsToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + vectorLength(): FunctionExpr; +} + +// @beta +export type ExprType = 'Field' | 'Constant' | 'Function' | 'AggregateFunction' | 'ListOfExprs' | 'ExprWithAlias'; + +// @beta (undocumented) +export class ExprWithAlias implements Selectable { + constructor(expr: Expr, alias: string); + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expr; + // (undocumented) + exprType: ExprType; + // (undocumented) + selectable: true; +} + +// @beta +export class Field extends Expr implements Selectable { + // (undocumented) + get alias(): string; + // (undocumented) + get expr(): Expr; + // (undocumented) + readonly exprType: ExprType; + // (undocumented) + fieldName(): string; + // (undocumented) + selectable: true; +} + +// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// +// @public +export function field(name: string): Field; + +// Warning: (ae-forgotten-export) The symbol "FieldPath" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// +// @public (undocumented) +export function field(path: FieldPath): Field; + +// @beta (undocumented) +export class FindNearest implements Stage { + // (undocumented) + name: string; +} + +// @beta (undocumented) +export interface FindNearestOptions { + // (undocumented) + distanceField?: string; + // (undocumented) + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + // (undocumented) + field: Field | string; + // (undocumented) + limit?: number; + // (undocumented) + vectorValue: VectorValue | number[]; +} + +// @beta +export class FunctionExpr extends Expr { + constructor(name: string, params: Expr[]); + // (undocumented) + readonly exprType: ExprType; + } + +// @beta (undocumented) +export class GenericStage implements Stage { + // (undocumented) + name: string; + } + +// @beta +export function gt(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function gt(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function gt(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function gt(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function gte(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function gte(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function gte(fieldName: string, value: Expr): BooleanExpr; + +// @beta +export function gte(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; + +// @beta +export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr; + +// @beta +export function isAbsent(value: Expr): BooleanExpr; + +// @beta +export function isAbsent(field: string): BooleanExpr; + +// @beta +export function isError(value: Expr): BooleanExpr; + +// @beta +export function isNan(value: Expr): BooleanExpr; + +// @beta +export function isNan(fieldName: string): BooleanExpr; + +// @beta +export function isNotNan(value: Expr): BooleanExpr; + +// @beta +export function isNotNan(value: string): BooleanExpr; + +// @beta +export function isNotNull(value: Expr): BooleanExpr; + +// @beta +export function isNotNull(value: string): BooleanExpr; + +// @beta +export function isNull(value: Expr): BooleanExpr; + +// @beta +export function isNull(value: string): BooleanExpr; + +// @beta +export function like(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function like(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function like(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta (undocumented) +export class Limit implements Stage { + constructor(limit: number, convertedFromLimitTolast?: boolean); + // (undocumented) + readonly convertedFromLimitTolast: boolean; + // (undocumented) + readonly limit: number; + // (undocumented) + name: string; +} + +// @beta +export function logicalMaximum(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMaximum(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMinimum(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMinimum(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function lt(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function lt(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function lt(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function lt(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function lte(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function lte(expression: Expr, value: unknown): BooleanExpr; + +// Warning: (ae-incompatible-release-tags) The symbol "lte" is marked as @public, but its signature references "Expr" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "lte" is marked as @public, but its signature references "BooleanExpr" which is marked as @beta +// +// @public +export function lte(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function lte(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function manhattanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function manhattanDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function manhattanDistance(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function manhattanDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function map(elements: Record): FunctionExpr; + +// @beta +export function mapGet(fieldName: string, subField: string): FunctionExpr; + +// @beta +export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; + +// @beta +export function mapMerge(mapField: string, secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + +// @beta +export function mapMerge(firstMap: Record | Expr, secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + +// @beta +export function mapRemove(mapField: string, key: string): FunctionExpr; + +// @beta +export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; + +// @beta +export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; + +// @beta +export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; + +// @beta +export function maximum(expression: Expr): AggregateFunction; + +// @beta +export function maximum(fieldName: string): AggregateFunction; + +// @beta +export function minimum(expression: Expr): AggregateFunction; + +// @beta +export function minimum(fieldName: string): AggregateFunction; + +// @beta +export function mod(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function mod(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function mod(fieldName: string, expression: Expr): FunctionExpr; + +// @beta +export function mod(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function multiply(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function multiply(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function neq(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function neq(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function neq(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function neq(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function not(booleanExpr: BooleanExpr): BooleanExpr; + +// @beta +export function notEqAny(element: Expr, values: Array): BooleanExpr; + +// @beta +export function notEqAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta (undocumented) +export class Offset implements Stage { + constructor(offset: number); + // (undocumented) + name: string; + } + +// @beta +export function or(first: BooleanExpr, second: BooleanExpr, ...more: BooleanExpr[]): BooleanExpr; + +// @beta +export class Ordering { + constructor(expr: Expr, direction: 'ascending' | 'descending'); + // (undocumented) + readonly direction: 'ascending' | 'descending'; + // (undocumented) + readonly expr: Expr; +} + +// @public +export class Pipeline { + /* Excluded from this release type: _db */ + // Warning: (ae-incompatible-release-tags) The symbol "addFields" is marked as @public, but its signature references "Selectable" which is marked as @beta + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "aggregate" is marked as @public, but its signature references "AggregateWithAlias" which is marked as @beta + aggregate(accumulator: AggregateWithAlias, ...additionalAccumulators: AggregateWithAlias[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + aggregate(options: { + accumulators: AggregateWithAlias[]; + groups?: Array; + }): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "distinct" is marked as @public, but its signature references "Selectable" which is marked as @beta + distinct(group: string | Selectable, ...additionalGroups: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "findNearest" is marked as @public, but its signature references "FindNearestOptions" which is marked as @beta + // + // (undocumented) + findNearest(options: FindNearestOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + genericStage(name: string, params: unknown[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + limit(limit: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + offset(offset: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "removeFields" is marked as @public, but its signature references "Field" which is marked as @beta + removeFields(fieldValue: Field | string, ...additionalFields: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "replaceWith" is marked as @public, but its signature references "Field" which is marked as @beta + replaceWith(fieldValue: Field | string): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sample(documents: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sample(options: { + percentage: number; + } | { + documents: number; + }): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "select" is marked as @public, but its signature references "Selectable" which is marked as @beta + select(selection: Selectable | string, ...additionalSelections: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "sort" is marked as @public, but its signature references "Ordering" which is marked as @beta + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + union(other: Pipeline): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "unnest" is marked as @public, but its signature references "Selectable" which is marked as @beta + unnest(selectable: Selectable, indexField?: string): Pipeline; + /* Excluded from this release type: _userDataWriter */ + // Warning: (ae-incompatible-release-tags) The symbol "where" is marked as @public, but its signature references "BooleanExpr" which is marked as @beta + where(condition: BooleanExpr): Pipeline; +} + +// Warning: (ae-forgotten-export) The symbol "DocumentData" needs to be exported by the entry point pipelines.d.ts +// +// @beta +export class PipelineResult { + /* Excluded from this release type: _ref */ + /* Excluded from this release type: _fields */ + /* Excluded from this release type: __constructor */ + get createTime(): Timestamp | undefined; + data(): AppModelType | undefined; + get(fieldPath: string | FieldPath | Field): any; + get id(): string | undefined; + get ref(): DocumentReference | undefined; + get updateTime(): Timestamp | undefined; +} + +// @public (undocumented) +export class PipelineSnapshot { + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "PipelineResult" which is marked as @beta + constructor(pipeline: Pipeline, results: PipelineResult[], executionTime?: Timestamp); + get executionTime(): Timestamp; + get pipeline(): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "results" is marked as @public, but its signature references "PipelineResult" which is marked as @beta + get results(): PipelineResult[]; +} + +// @beta +export class PipelineSource { + collection(collectionPath: string): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + // Warning: (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts + collection(collectionReference: Query): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + collectionGroup(collectionId: string): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + createFrom(query: Query): Pipeline; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + database(): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + documents(docs: Array): PipelineType; +} + +// @beta +export function rand(): FunctionExpr; + +// @beta +export function regexContains(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function regexContains(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta +export function regexMatch(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function regexMatch(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta +export function replaceAll(value: Expr, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceAll(value: Expr, find: Expr, replace: Expr): FunctionExpr; + +// @beta +export function replaceAll(fieldName: string, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceFirst(value: Expr, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceFirst(value: Expr, find: Expr, replace: Expr): FunctionExpr; + +// @beta +export function replaceFirst(fieldName: string, find: string, replace: string): FunctionExpr; + +// @beta +export function reverse(stringExpression: Expr): FunctionExpr; + +// @beta +export function reverse(field: string): FunctionExpr; + +// @beta (undocumented) +export class Select implements Stage { + constructor(projections: Map); + // (undocumented) + name: string; + } + +// @beta +export interface Selectable { + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expr; + // (undocumented) + selectable: true; +} + +// @beta (undocumented) +export class Sort implements Stage { + constructor(orders: Ordering[]); + // (undocumented) + name: string; + } + +// @beta (undocumented) +export interface Stage { + // (undocumented) + name: string; +} + +// @beta +export function startsWith(fieldName: string, prefix: string): BooleanExpr; + +// @beta +export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; + +// @beta +export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; + +// @beta +export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; + +// @beta +export function strConcat(fieldName: string, secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + +// @beta +export function strConcat(firstString: Expr, secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + +// @beta +export function strContains(fieldName: string, substring: string): BooleanExpr; + +// @beta +export function strContains(fieldName: string, substring: Expr): BooleanExpr; + +// @beta +export function strContains(stringExpression: Expr, substring: string): BooleanExpr; + +// @beta +export function strContains(stringExpression: Expr, substring: Expr): BooleanExpr; + +// @beta +export function substr(field: string, position: number, length?: number): FunctionExpr; + +// @beta +export function substr(input: Expr, position: number, length?: number): FunctionExpr; + +// @beta +export function substr(field: string, position: Expr, length?: Expr): FunctionExpr; + +// @beta +export function substr(input: Expr, position: Expr, length?: Expr): FunctionExpr; + +// @beta +export function subtract(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function subtract(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function subtract(fieldName: string, expression: Expr): FunctionExpr; + +// @beta +export function subtract(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): FunctionExpr; + +// @beta +export function timestampAdd(timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampAdd(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampSub(timestamp: Expr, unit: Expr, amount: Expr): FunctionExpr; + +// @beta +export function timestampSub(timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampSub(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampToUnixMicros(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixMicros(fieldName: string): FunctionExpr; + +// @beta +export function timestampToUnixMillis(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixMillis(fieldName: string): FunctionExpr; + +// @beta +export function timestampToUnixSeconds(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixSeconds(fieldName: string): FunctionExpr; + +// @beta +export function toLower(fieldName: string): FunctionExpr; + +// @beta +export function toLower(stringExpression: Expr): FunctionExpr; + +// @beta +export function toUpper(fieldName: string): FunctionExpr; + +// @beta +export function toUpper(stringExpression: Expr): FunctionExpr; + +// @beta +export function trim(fieldName: string): FunctionExpr; + +// @beta +export function trim(stringExpression: Expr): FunctionExpr; + +// @beta +export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function unixMillisToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixMillisToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function vectorLength(vectorExpression: Expr): FunctionExpr; + +// @beta +export function vectorLength(fieldName: string): FunctionExpr; + +// @beta (undocumented) +export class Where implements Stage { + constructor(condition: BooleanExpr); + // (undocumented) + name: string; +} + +// @beta +export function xor(first: BooleanExpr, second: BooleanExpr, ...additionalConditions: BooleanExpr[]): BooleanExpr; + + +// Warnings were encountered during analysis: +// +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:4613:9 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AggregateWithAlias" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:4614:9 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta + +// (No @packageDocumentation comment for this package) + +``` diff --git a/common/api-review/firestore-pipelines.api.md b/common/api-review/firestore-pipelines.api.md new file mode 100644 index 00000000000..baedbf6b08a --- /dev/null +++ b/common/api-review/firestore-pipelines.api.md @@ -0,0 +1,1299 @@ +## API Report File for "@firebase/firestore-pipelines" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app'; + +// @beta +export function add(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function add(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta (undocumented) +export class AddFields implements Stage { + constructor(fields: Map); + // (undocumented) + name: string; +} + +// @beta (undocumented) +export class Aggregate implements Stage { + constructor(accumulators: Map, groups: Map); + // (undocumented) + name: string; +} + +// @beta +export class AggregateFunction { + constructor(name: string, params: Expr[]); + as(name: string): AggregateWithAlias; + // (undocumented) + exprType: ExprType; + } + +// @beta +export class AggregateWithAlias { + constructor(aggregate: AggregateFunction, alias: string); + // (undocumented) + readonly aggregate: AggregateFunction; + // (undocumented) + readonly alias: string; +} + +// @beta +export function and(first: BooleanExpr, second: BooleanExpr, ...more: BooleanExpr[]): BooleanExpr; + +// @beta +export function array(elements: unknown[]): FunctionExpr; + +// @beta +export function arrayConcat(firstArray: Expr, secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + +// @beta +export function arrayConcat(firstArrayField: string, secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + +// @beta +export function arrayContains(array: Expr, element: Expr): FunctionExpr; + +// @beta +export function arrayContains(array: Expr, element: unknown): FunctionExpr; + +// @beta +export function arrayContains(fieldName: string, element: Expr): FunctionExpr; + +// @beta +export function arrayContains(fieldName: string, element: unknown): BooleanExpr; + +// @beta +export function arrayContainsAll(array: Expr, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAll(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAll(array: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function arrayContainsAll(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta +export function arrayContainsAny(array: Expr, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; + +// @beta +export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; + +// @beta +export function arrayLength(array: Expr): FunctionExpr; + +// @beta +export function arrayOffset(arrayField: string, offset: number): FunctionExpr; + +// @beta +export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; + +// @beta +export function arrayOffset(arrayExpression: Expr, offset: number): FunctionExpr; + +// @beta +export function arrayOffset(arrayExpression: Expr, offsetExpr: Expr): FunctionExpr; + +// @beta +export function ascending(expr: Expr): Ordering; + +// @beta +export function ascending(fieldName: string): Ordering; + +// @beta +export function avg(expression: Expr): AggregateFunction; + +// @beta +export function avg(fieldName: string): AggregateFunction; + +// Warning: (ae-forgotten-export) The symbol "Bytes" needs to be exported by the entry point pipelines.d.ts +// +// @beta +export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitAnd(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitAnd(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export function bitLeftShift(field: string, y: number): FunctionExpr; + +// @beta +export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; + +// @beta +export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitNot(field: string): FunctionExpr; + +// @beta +export function bitNot(bitsValueExpression: Expr): FunctionExpr; + +// @beta +export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitOr(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitOr(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export function bitRightShift(field: string, y: number): FunctionExpr; + +// @beta +export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitRightShift(xValue: Expr, y: number): FunctionExpr; + +// @beta +export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; + +// @beta +export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; + +// @beta +export function bitXor(bitsExpression: Expr, otherBits: number | Bytes): FunctionExpr; + +// @beta +export function bitXor(bitsExpression: Expr, otherBitsExpression: Expr): FunctionExpr; + +// @beta +export class BooleanExpr extends FunctionExpr { + countIf(): AggregateFunction; + // (undocumented) + filterable: true; + not(): BooleanExpr; +} + +// @beta +export function byteLength(expr: Expr): FunctionExpr; + +// @beta +export function byteLength(fieldName: string): FunctionExpr; + +// @beta +export function charLength(fieldName: string): FunctionExpr; + +// @beta +export function charLength(stringExpression: Expr): FunctionExpr; + +// @beta (undocumented) +export class CollectionGroupSource implements Stage { + constructor(collectionId: string); + // (undocumented) + name: string; +} + +// @beta (undocumented) +export class CollectionSource implements Stage { + constructor(collectionPath: string); + // (undocumented) + name: string; +} + +// @beta +export function cond(condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr): FunctionExpr; + +// @beta +export class Constant extends Expr { + // (undocumented) + readonly exprType: ExprType; + } + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: number): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: string): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: boolean): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: null): Constant; + +// Warning: (ae-forgotten-export) The symbol "GeoPoint" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: GeoPoint): Constant; + +// Warning: (ae-forgotten-export) The symbol "Timestamp" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Timestamp): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Date): Constant; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: Bytes): Constant; + +// Warning: (ae-forgotten-export) The symbol "DocumentReference" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: DocumentReference): Constant; + +// Warning: (ae-forgotten-export) The symbol "VectorValue" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Constant" which is marked as @beta +// +// @public +export function constant(value: VectorValue): Constant; + +// @beta +export function cosineDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function cosineDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function cosineDistance(vectorExpression: Expr, vector: number[] | Expr): FunctionExpr; + +// @beta +export function cosineDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function count(expression: Expr): AggregateFunction; + +// Warning: (ae-incompatible-release-tags) The symbol "count" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta +// +// @public +export function count(fieldName: string): AggregateFunction; + +// @beta +export function countAll(): AggregateFunction; + +// @beta +export function countIf(booleanExpr: BooleanExpr): AggregateFunction; + +// @beta +export function currentContext(): FunctionExpr; + +// @beta (undocumented) +export class DatabaseSource implements Stage { + // (undocumented) + name: string; +} + +// @beta +export function descending(expr: Expr): Ordering; + +// @beta +export function descending(fieldName: string): Ordering; + +// @beta (undocumented) +export class Distinct implements Stage { + constructor(groups: Map); + // (undocumented) + name: string; +} + +// @beta +export function divide(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function divide(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function divide(fieldName: string, expressions: Expr): FunctionExpr; + +// @beta +export function divide(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function documentId(documentPath: string | DocumentReference): FunctionExpr; + +// @beta +export function documentId(documentPathExpr: Expr): FunctionExpr; + +// @beta (undocumented) +export class DocumentsSource implements Stage { + constructor(docPaths: string[]); + // (undocumented) + name: string; + // (undocumented) + static of(refs: Array): DocumentsSource; +} + +// @beta +export function dotProduct(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function dotProduct(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function dotProduct(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function dotProduct(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function endsWith(fieldName: string, suffix: string): BooleanExpr; + +// @beta +export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; + +// @beta +export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; + +// @beta +export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; + +// @beta +export function eq(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function eq(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function eq(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function eq(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function eqAny(expression: Expr, values: Array): BooleanExpr; + +// @beta +export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function eqAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta +export function euclideanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function euclideanDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function euclideanDistance(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function euclideanDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @public +export function execute(pipeline: Pipeline): Promise; + +// @beta +export function exists(value: Expr): BooleanExpr; + +// @beta +export function exists(fieldName: string): BooleanExpr; + +// @beta +export abstract class Expr { + add(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayConcat(secondArray: Expr | unknown[], ...otherArrays: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayContains(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContains(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + arrayLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayOffset(offset: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + arrayOffset(offsetExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + as(name: string): ExprWithAlias; + /* Excluded from this release type: _readUserData */ + ascending(): Ordering; + /* Excluded from this release type: _readUserData */ + avg(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + bitAnd(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitAnd(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitLeftShift(y: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitLeftShift(numberExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitNot(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitOr(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitOr(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitRightShift(y: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitRightShift(numberExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitXor(otherBits: number | Bytes): FunctionExpr; + /* Excluded from this release type: _readUserData */ + bitXor(bitsExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + byteLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + charLength(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + cosineDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + cosineDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + count(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + descending(): Ordering; + /* Excluded from this release type: _readUserData */ + divide(other: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + divide(other: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + documentId(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + dotProduct(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + dotProduct(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eq(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eq(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eqAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + eqAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + exists(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + // (undocumented) + abstract readonly exprType: ExprType; + /* Excluded from this release type: _readUserData */ + gt(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gt(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gte(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + gte(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + ifError(catchExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + ifError(catchValue: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + isAbsent(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isError(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNan(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNotNan(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNotNull(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + isNull(): BooleanExpr; + /* Excluded from this release type: _readUserData */ + like(pattern: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + like(pattern: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + logicalMaximum(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + logicalMinimum(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + lt(experession: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lt(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lte(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + lte(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + manhattanDistance(vector: VectorValue | number[]): FunctionExpr; + /* Excluded from this release type: _readUserData */ + manhattanDistance(vectorExpression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapGet(subfield: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapMerge(secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapRemove(key: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mapRemove(keyExpr: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + maximum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + minimum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + mod(expression: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + mod(value: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + multiply(second: Expr | unknown, ...others: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + neq(expression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + neq(value: unknown): BooleanExpr; + /* Excluded from this release type: _readUserData */ + notEqAny(values: Array): BooleanExpr; + /* Excluded from this release type: _readUserData */ + notEqAny(arrayExpression: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + replaceAll(find: string, replace: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceAll(find: Expr, replace: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceFirst(find: string, replace: string): FunctionExpr; + /* Excluded from this release type: _readUserData */ + replaceFirst(find: Expr, replace: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + reverse(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + strConcat(secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + /* Excluded from this release type: _readUserData */ + strContains(substring: string): BooleanExpr; + /* Excluded from this release type: _readUserData */ + strContains(expr: Expr): BooleanExpr; + /* Excluded from this release type: _readUserData */ + substr(position: number, length?: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + substr(position: Expr, length?: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + subtract(other: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + subtract(other: unknown): FunctionExpr; + /* Excluded from this release type: _readUserData */ + sum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: Expr, amount: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampSub(unit: Expr, amount: Expr): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampSub(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixMicros(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixMillis(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + timestampToUnixSeconds(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + toLower(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + toUpper(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + trim(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixMicrosToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixMillisToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + unixSecondsToTimestamp(): FunctionExpr; + /* Excluded from this release type: _readUserData */ + vectorLength(): FunctionExpr; +} + +// @beta +export type ExprType = 'Field' | 'Constant' | 'Function' | 'AggregateFunction' | 'ListOfExprs' | 'ExprWithAlias'; + +// @beta (undocumented) +export class ExprWithAlias implements Selectable { + constructor(expr: Expr, alias: string); + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expr; + // (undocumented) + exprType: ExprType; + // (undocumented) + selectable: true; +} + +// @beta +export class Field extends Expr implements Selectable { + // (undocumented) + get alias(): string; + // (undocumented) + get expr(): Expr; + // (undocumented) + readonly exprType: ExprType; + // (undocumented) + fieldName(): string; + // (undocumented) + selectable: true; +} + +// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// +// @public +export function field(name: string): Field; + +// Warning: (ae-forgotten-export) The symbol "FieldPath" needs to be exported by the entry point pipelines.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// +// @public (undocumented) +export function field(path: FieldPath): Field; + +// @beta (undocumented) +export class FindNearest implements Stage { + // (undocumented) + name: string; +} + +// @beta (undocumented) +export interface FindNearestOptions { + // (undocumented) + distanceField?: string; + // (undocumented) + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + // (undocumented) + field: Field | string; + // (undocumented) + limit?: number; + // (undocumented) + vectorValue: VectorValue | number[]; +} + +// @beta +export class FunctionExpr extends Expr { + constructor(name: string, params: Expr[]); + // (undocumented) + readonly exprType: ExprType; + } + +// @beta (undocumented) +export class GenericStage implements Stage { + // (undocumented) + name: string; + } + +// @beta +export function gt(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function gt(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function gt(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function gt(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function gte(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function gte(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function gte(fieldName: string, value: Expr): BooleanExpr; + +// @beta +export function gte(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; + +// @beta +export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr; + +// @beta +export function isAbsent(value: Expr): BooleanExpr; + +// @beta +export function isAbsent(field: string): BooleanExpr; + +// @beta +export function isError(value: Expr): BooleanExpr; + +// @beta +export function isNan(value: Expr): BooleanExpr; + +// @beta +export function isNan(fieldName: string): BooleanExpr; + +// @beta +export function isNotNan(value: Expr): BooleanExpr; + +// @beta +export function isNotNan(value: string): BooleanExpr; + +// @beta +export function isNotNull(value: Expr): BooleanExpr; + +// @beta +export function isNotNull(value: string): BooleanExpr; + +// @beta +export function isNull(value: Expr): BooleanExpr; + +// @beta +export function isNull(value: string): BooleanExpr; + +// @beta +export function like(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function like(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function like(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta (undocumented) +export class Limit implements Stage { + constructor(limit: number, convertedFromLimitTolast?: boolean); + // (undocumented) + readonly convertedFromLimitTolast: boolean; + // (undocumented) + readonly limit: number; + // (undocumented) + name: string; +} + +// @beta +export function logicalMaximum(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMaximum(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMinimum(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function logicalMinimum(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function lt(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function lt(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function lt(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function lt(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function lte(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function lte(expression: Expr, value: unknown): BooleanExpr; + +// Warning: (ae-incompatible-release-tags) The symbol "lte" is marked as @public, but its signature references "Expr" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "lte" is marked as @public, but its signature references "BooleanExpr" which is marked as @beta +// +// @public +export function lte(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function lte(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function manhattanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function manhattanDistance(fieldName: string, vectorExpression: Expr): FunctionExpr; + +// @beta +export function manhattanDistance(vectorExpression: Expr, vector: number[] | VectorValue): FunctionExpr; + +// @beta +export function manhattanDistance(vectorExpression: Expr, otherVectorExpression: Expr): FunctionExpr; + +// @beta +export function map(elements: Record): FunctionExpr; + +// @beta +export function mapGet(fieldName: string, subField: string): FunctionExpr; + +// @beta +export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; + +// @beta +export function mapMerge(mapField: string, secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + +// @beta +export function mapMerge(firstMap: Record | Expr, secondMap: Record | Expr, ...otherMaps: Array | Expr>): FunctionExpr; + +// @beta +export function mapRemove(mapField: string, key: string): FunctionExpr; + +// @beta +export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; + +// @beta +export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; + +// @beta +export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; + +// @beta +export function maximum(expression: Expr): AggregateFunction; + +// @beta +export function maximum(fieldName: string): AggregateFunction; + +// @beta +export function minimum(expression: Expr): AggregateFunction; + +// @beta +export function minimum(fieldName: string): AggregateFunction; + +// @beta +export function mod(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function mod(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function mod(fieldName: string, expression: Expr): FunctionExpr; + +// @beta +export function mod(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function multiply(first: Expr, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function multiply(fieldName: string, second: Expr | unknown, ...others: Array): FunctionExpr; + +// @beta +export function neq(left: Expr, right: Expr): BooleanExpr; + +// @beta +export function neq(expression: Expr, value: unknown): BooleanExpr; + +// @beta +export function neq(fieldName: string, expression: Expr): BooleanExpr; + +// @beta +export function neq(fieldName: string, value: unknown): BooleanExpr; + +// @beta +export function not(booleanExpr: BooleanExpr): BooleanExpr; + +// @beta +export function notEqAny(element: Expr, values: Array): BooleanExpr; + +// @beta +export function notEqAny(fieldName: string, values: Array): BooleanExpr; + +// @beta +export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; + +// @beta +export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +// @beta (undocumented) +export class Offset implements Stage { + constructor(offset: number); + // (undocumented) + name: string; + } + +// @beta +export function or(first: BooleanExpr, second: BooleanExpr, ...more: BooleanExpr[]): BooleanExpr; + +// @beta +export class Ordering { + constructor(expr: Expr, direction: 'ascending' | 'descending'); + // (undocumented) + readonly direction: 'ascending' | 'descending'; + // (undocumented) + readonly expr: Expr; +} + +// @public (undocumented) +export class Pipeline { + // Warning: (ae-incompatible-release-tags) The symbol "addFields" is marked as @public, but its signature references "Selectable" which is marked as @beta + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "aggregate" is marked as @public, but its signature references "AggregateWithAlias" which is marked as @beta + aggregate(accumulator: AggregateWithAlias, ...additionalAccumulators: AggregateWithAlias[]): Pipeline; + aggregate(options: { accumulators: AggregateWithAlias[]; groups?: Array; }): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "distinct" is marked as @public, but its signature references "Selectable" which is marked as @beta + distinct(group: string | Selectable, ...additionalGroups: Array): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "findNearest" is marked as @public, but its signature references "FindNearestOptions" which is marked as @beta + // + // (undocumented) + findNearest(options: FindNearestOptions): Pipeline; + genericStage(name: string, params: unknown[]): Pipeline; + limit(limit: number): Pipeline; + offset(offset: number): Pipeline; + readUserData: any; + // Warning: (ae-incompatible-release-tags) The symbol "removeFields" is marked as @public, but its signature references "Field" which is marked as @beta + removeFields(fieldValue: Field | string, ...additionalFields: Array): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "replaceWith" is marked as @public, but its signature references "Field" which is marked as @beta + replaceWith(fieldValue: Field | string): Pipeline; + sample(documents: number): Pipeline; + sample(options: { percentage: number; } | { documents: number; }): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "select" is marked as @public, but its signature references "Selectable" which is marked as @beta + select(selection: Selectable | string, ...additionalSelections: Array): Pipeline; + // (undocumented) + selectablesToMap: any; + // Warning: (ae-incompatible-release-tags) The symbol "sort" is marked as @public, but its signature references "Ordering" which is marked as @beta + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; + // (undocumented) + stages: any; + union(other: Pipeline): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "unnest" is marked as @public, but its signature references "Selectable" which is marked as @beta + unnest(selectable: Selectable, indexField?: string): Pipeline; + // (undocumented) + userDataReader: any; + // Warning: (ae-incompatible-release-tags) The symbol "where" is marked as @public, but its signature references "BooleanExpr" which is marked as @beta + where(condition: BooleanExpr): Pipeline; +} + +// Warning: (ae-forgotten-export) The symbol "DocumentData" needs to be exported by the entry point pipelines.d.ts +// +// @beta +export class PipelineResult { + /* Excluded from this release type: _ref */ + /* Excluded from this release type: _fields */ + /* Excluded from this release type: __constructor */ + get createTime(): Timestamp | undefined; + data(): AppModelType | undefined; + get(fieldPath: string | FieldPath | Field): any; + get id(): string | undefined; + get ref(): DocumentReference | undefined; + get updateTime(): Timestamp | undefined; +} + +// @public (undocumented) +export class PipelineSnapshot { + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "PipelineResult" which is marked as @beta + constructor(pipeline: Pipeline, results: PipelineResult[], executionTime?: Timestamp); + get executionTime(): Timestamp; + get pipeline(): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "results" is marked as @public, but its signature references "PipelineResult" which is marked as @beta + get results(): PipelineResult[]; +} + +// @beta +export class PipelineSource { + collection(collectionPath: string): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + // Warning: (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts + collection(collectionReference: Query): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + collectionGroup(collectionId: string): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + createFrom(query: Query): Pipeline; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + database(): PipelineType; + /* Excluded from this release type: _createPipeline */ + /* Excluded from this release type: __constructor */ + documents(docs: Array): PipelineType; +} + +// @beta +export function rand(): FunctionExpr; + +// @beta +export function regexContains(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function regexContains(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function regexContains(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta +export function regexMatch(fieldName: string, pattern: string): BooleanExpr; + +// @beta +export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; + +// @beta +export function regexMatch(stringExpression: Expr, pattern: string): BooleanExpr; + +// @beta +export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; + +// @beta +export function replaceAll(value: Expr, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceAll(value: Expr, find: Expr, replace: Expr): FunctionExpr; + +// @beta +export function replaceAll(fieldName: string, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceFirst(value: Expr, find: string, replace: string): FunctionExpr; + +// @beta +export function replaceFirst(value: Expr, find: Expr, replace: Expr): FunctionExpr; + +// @beta +export function replaceFirst(fieldName: string, find: string, replace: string): FunctionExpr; + +// @beta +export function reverse(stringExpression: Expr): FunctionExpr; + +// @beta +export function reverse(field: string): FunctionExpr; + +// @beta (undocumented) +export class Select implements Stage { + constructor(projections: Map); + // (undocumented) + name: string; + } + +// @beta +export interface Selectable { + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expr; + // (undocumented) + selectable: true; +} + +// @beta (undocumented) +export class Sort implements Stage { + constructor(orders: Ordering[]); + // (undocumented) + name: string; + } + +// @beta (undocumented) +export interface Stage { + // (undocumented) + name: string; +} + +// @beta +export function startsWith(fieldName: string, prefix: string): BooleanExpr; + +// @beta +export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; + +// @beta +export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; + +// @beta +export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; + +// @beta +export function strConcat(fieldName: string, secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + +// @beta +export function strConcat(firstString: Expr, secondString: Expr | string, ...otherStrings: Array): FunctionExpr; + +// @beta +export function strContains(fieldName: string, substring: string): BooleanExpr; + +// @beta +export function strContains(fieldName: string, substring: Expr): BooleanExpr; + +// @beta +export function strContains(stringExpression: Expr, substring: string): BooleanExpr; + +// @beta +export function strContains(stringExpression: Expr, substring: Expr): BooleanExpr; + +// @beta +export function substr(field: string, position: number, length?: number): FunctionExpr; + +// @beta +export function substr(input: Expr, position: number, length?: number): FunctionExpr; + +// @beta +export function substr(field: string, position: Expr, length?: Expr): FunctionExpr; + +// @beta +export function substr(input: Expr, position: Expr, length?: Expr): FunctionExpr; + +// @beta +export function subtract(left: Expr, right: Expr): FunctionExpr; + +// @beta +export function subtract(expression: Expr, value: unknown): FunctionExpr; + +// @beta +export function subtract(fieldName: string, expression: Expr): FunctionExpr; + +// @beta +export function subtract(fieldName: string, value: unknown): FunctionExpr; + +// @beta +export function sum(expression: Expr): AggregateFunction; + +// @beta +export function sum(fieldName: string): AggregateFunction; + +// @beta +export function timestampAdd(timestamp: Expr, unit: Expr, amount: Expr): FunctionExpr; + +// @beta +export function timestampAdd(timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampAdd(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampSub(timestamp: Expr, unit: Expr, amount: Expr): FunctionExpr; + +// @beta +export function timestampSub(timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampSub(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpr; + +// @beta +export function timestampToUnixMicros(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixMicros(fieldName: string): FunctionExpr; + +// @beta +export function timestampToUnixMillis(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixMillis(fieldName: string): FunctionExpr; + +// @beta +export function timestampToUnixSeconds(expr: Expr): FunctionExpr; + +// @beta +export function timestampToUnixSeconds(fieldName: string): FunctionExpr; + +// @beta +export function toLower(fieldName: string): FunctionExpr; + +// @beta +export function toLower(stringExpression: Expr): FunctionExpr; + +// @beta +export function toUpper(fieldName: string): FunctionExpr; + +// @beta +export function toUpper(stringExpression: Expr): FunctionExpr; + +// @beta +export function trim(fieldName: string): FunctionExpr; + +// @beta +export function trim(stringExpression: Expr): FunctionExpr; + +// @beta +export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function unixMillisToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixMillisToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; + +// @beta +export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; + +// @beta +export function vectorLength(vectorExpression: Expr): FunctionExpr; + +// @beta +export function vectorLength(fieldName: string): FunctionExpr; + +// @beta (undocumented) +export class Where implements Stage { + constructor(condition: BooleanExpr); + // (undocumented) + name: string; +} + +// @beta +export function xor(first: BooleanExpr, second: BooleanExpr, ...additionalConditions: BooleanExpr[]): BooleanExpr; + + +// Warnings were encountered during analysis: +// +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:4552:26 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AggregateWithAlias" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:4552:62 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta + +// (No @packageDocumentation comment for this package) + +``` diff --git a/package.json b/package.json index 30b6b09a003..00f6bdc5f80 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "postinstall-postinstall": "2.1.0", "prettier": "2.8.8", "protractor": "5.4.2", + "protobufjs-cli": "^1.1.3", "request": "2.88.2", "semver": "7.7.1", "simple-git": "3.27.0", diff --git a/packages/firebase/firestore/pipelines/index.ts b/packages/firebase/firestore/pipelines/index.ts new file mode 100644 index 00000000000..be062f16e96 --- /dev/null +++ b/packages/firebase/firestore/pipelines/index.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '@firebase/firestore/pipelines'; diff --git a/packages/firebase/firestore/pipelines/package.json b/packages/firebase/firestore/pipelines/package.json new file mode 100644 index 00000000000..e63cb928b0f --- /dev/null +++ b/packages/firebase/firestore/pipelines/package.json @@ -0,0 +1,7 @@ +{ + "name": "firebase/firestore/pipelines", + "main": "dist/pipelines.cjs.js", + "browser": "dist/esm/pipelines.esm.js", + "module": "dist/esm/pipelines.esm.js", + "typings": "dist/firestore/lite/pipelines.d.ts" +} diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 4f785d15c8c..0a7ec65fbe1 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -131,6 +131,18 @@ }, "default": "./firestore/dist/esm/index.esm.js" }, + "./firestore/pipelines": { + "types": "./firestore/dist/firestore/pipelines.d.ts", + "node": { + "require": "./firestore/dist/pipelines.cjs.js", + "import": "./firestore/dist/pipelines.mjs" + }, + "browser": { + "require": "./firestore/dist/pipelines.cjs.js", + "import": "./firestore/dist/esm/pipelines.esm.js" + }, + "default": "./firestore/dist/esm/pipelines.esm.js" + }, "./firestore/lite": { "types": "./firestore/lite/dist/firestore/lite/index.d.ts", "node": { diff --git a/packages/firestore/.eslintrc.js b/packages/firestore/.eslintrc.js index 5dd443333d9..9ffb1d0279b 100644 --- a/packages/firestore/.eslintrc.js +++ b/packages/firestore/.eslintrc.js @@ -24,7 +24,7 @@ module.exports = { tsconfigRootDir: __dirname }, plugins: ['import'], - ignorePatterns: ['compat/*'], + ignorePatterns: ['compat/*', 'pipelines.d.ts'], rules: { 'no-console': ['error', { allow: ['warn', 'error'] }], '@typescript-eslint/no-unused-vars': [ diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index c56b078dddf..ae68fe87be8 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -17,7 +17,9 @@ "packages/app-check-interop-types/index.d.ts", "packages/auth-interop-types/index.d.ts", "packages/firestore/dist/lite/internal.d.ts", + "packages/firestore/dist/lite/internal.pipelines.d.ts", "packages/firestore/dist/internal.d.ts", + "packages/firestore/dist/internal.pipelines.d.ts", "packages/firestore-types/index.d.ts", "packages/firebase/compat/index.d.ts", "packages/component/dist/src/component.d.ts", diff --git a/packages/firestore/lite/pipelines/package.json b/packages/firestore/lite/pipelines/package.json new file mode 100644 index 00000000000..e7989c2ea97 --- /dev/null +++ b/packages/firestore/lite/pipelines/package.json @@ -0,0 +1,14 @@ +{ + "name": "@firebase/firestore-lite-pipelines", + "description": "Pipelines for the lite Firestore SDK", + "main": "../../dist/lite/pipelines.node.cjs.js", + "main-esm": "../../dist/lite/pipelines.node.mjs", + "module": "../../dist/lite/pipelines.browser.esm2017.js", + "browser": "../../dist/lite/pipelines.browser.esm2017.js", + "react-native": "../../dist/lite/pipelines.rn.esm2017.js", + "typings": "./pipelines.d.ts", + "private": true, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/firestore/lite/pipelines/pipelines.d.ts b/packages/firestore/lite/pipelines/pipelines.d.ts new file mode 100644 index 00000000000..81336456656 --- /dev/null +++ b/packages/firestore/lite/pipelines/pipelines.d.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PipelineSource, Pipeline } from '../../dist/lite/pipelines'; + +// Augument the Firestore class with the pipeline() method. +// This is stripped from dist/lite/pipelines.d.ts during the build +// so it needs to be re-added here. +declare module '@firebase/firestore/lite' { + interface Firestore { + pipeline(): PipelineSource; + } +} + +export * from '../../dist/lite/pipelines'; diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts new file mode 100644 index 00000000000..cc7f91e750c --- /dev/null +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -0,0 +1,179 @@ +/** + * Firestore Lite Pipelines + * + * @remarks Firestore Lite is a small online-only SDK that allows read + * and write access to your Firestore database. All operations connect + * directly to the backend, and `onSnapshot()` APIs are not supported. + * @packageDocumentation + */ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// External exports: ./index +// These external exports will be stripped from the dist/pipelines.d.ts file +// by the prune-dts script, in order to reduce type duplication. However, these +// types need to be exported here to ensure that api-extractor behaves +// correctly. If a type from api.ts is missing from this export, then +// api-extractor may rename it with a suffix `_#`, e.g. `YourType_2`. +export type { + Timestamp, + DocumentReference, + VectorValue, + GeoPoint, + FieldPath, + DocumentData, + Query, + Firestore, + FirestoreDataConverter, + WithFieldValue, + PartialWithFieldValue, + SetOptions, + QueryDocumentSnapshot, + Primitive, + FieldValue, + Bytes +} from '../index'; + +export { PipelineSource } from '../../src/lite-api/pipeline-source'; + +export { + PipelineResult, + PipelineSnapshot +} from '../../src/lite-api/pipeline-result'; + +export { Pipeline } from '../../src/lite-api/pipeline'; + +export { execute } from '../../src/lite-api/pipeline_impl'; + +export { + Stage, + FindNearestOptions, + AddFields, + Aggregate, + Distinct, + CollectionSource, + CollectionGroupSource, + DatabaseSource, + DocumentsSource, + Where, + FindNearest, + Limit, + Offset, + Select, + Sort, + GenericStage +} from '../../src/lite-api/stage'; + +export { + Expr, + field, + and, + array, + arrayOffset, + constant, + add, + subtract, + multiply, + avg, + bitAnd, + substr, + constantVector, + bitLeftShift, + bitNot, + count, + mapMerge, + mapRemove, + bitOr, + ifError, + isAbsent, + isError, + or, + rand, + bitRightShift, + bitXor, + divide, + isNotNan, + map, + isNotNull, + isNull, + mod, + documentId, + eq, + neq, + lt, + countIf, + lte, + gt, + gte, + arrayConcat, + arrayContains, + arrayContainsAny, + arrayContainsAll, + arrayLength, + eqAny, + notEqAny, + xor, + cond, + not, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + replaceFirst, + replaceAll, + byteLength, + charLength, + like, + regexContains, + regexMatch, + strContains, + startsWith, + endsWith, + toLower, + toUpper, + trim, + strConcat, + mapGet, + countAll, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSub, + ascending, + descending, + ExprWithAlias, + Field, + Constant, + FunctionExpr, + Ordering, + ExprType, + AggregateWithAlias, + Selectable, + BooleanExpr, + AggregateFunction +} from '../../src/lite-api/expressions'; diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 638b8914483..8d849c48d27 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -50,11 +50,13 @@ "test:minified": "(cd ../../integration/firestore ; yarn test)", "trusted-type-check": "tsec -p tsconfig.json --noEmit", "api-report:main": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package firestore --packageRoot . --typescriptDts ./dist/firestore/src/index.d.ts --rollupDts ./dist/private.d.ts --untrimmedRollupDts ./dist/internal.d.ts --publicDts ./dist/index.d.ts", + "api-report:pipelines": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package firestore-pipelines --packageRoot . --typescriptDts ./dist/firestore/pipelines/pipelines.d.ts --rollupDts ./dist/private.pipelines.d.ts --untrimmedRollupDts ./dist/internal.pipelines.d.ts --publicDts ./dist/pipelines.d.ts --otherExportsPublicDtsFiles ./dist/index.d.ts", "api-report:lite": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package firestore-lite --packageRoot . --typescriptDts ./dist/firestore/lite/index.d.ts --rollupDts ./dist/lite/private.d.ts --untrimmedRollupDts ./dist/lite/internal.d.ts --publicDts ./dist/lite/index.d.ts", + "api-report:lite:pipelines": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package firestore-lite-pipelines --packageRoot . --typescriptDts ./dist/firestore/lite/pipelines/pipelines.d.ts --rollupDts ./dist/lite/private.pipelines.d.ts --untrimmedRollupDts ./dist/lite/internal.pipelines.d.ts --publicDts ./dist/lite/pipelines.d.ts --otherExportsPublicDtsFiles ./dist/lite/index.d.ts", "api-report:api-json": "rm -rf temp && api-extractor run --local --verbose", - "api-report": "run-s --npm-path npm api-report:main api-report:lite && yarn api-report:api-json", + "api-report": "run-s --npm-path npm api-report:main api-report:pipelines api-report:lite api-report:lite:pipelines && yarn api-report:api-json", "doc": "api-documenter markdown --input temp --output docs", - "typings:public": "node ../../scripts/build/use_typings.js ./dist/index.d.ts", + "typings:public": "node ../../scripts/build/use_typings.js ./dist/all-packages.d.ts", "assertion-id:check": "ts-node scripts/assertion-id-tool.ts --dir=src --check", "assertion-id:new": "ts-node scripts/assertion-id-tool.ts --dir=src --new", "assertion-id:list": "ts-node scripts/assertion-id-tool.ts --dir=src --list", @@ -82,18 +84,44 @@ }, "react-native": "./dist/lite/index.rn.esm2017.js", "browser": { - "require": "./dist/lite/index.cjs.js", + "require": "./dist/lite/index.browser.cjs.js", "import": "./dist/lite/index.browser.esm2017.js" }, "default": "./dist/lite/index.browser.esm2017.js" }, + "./lite/pipelines": { + "types": "./dist/lite/pipelines.d.ts", + "node": { + "require": "./dist/lite/pipelines.node.cjs.js", + "import": "./dist/lite/pipelines.node.mjs" + }, + "react-native": "./dist/lite/pipelines.rn.esm2017.js", + "browser": { + "require": "./dist/lite/pipelines.browser.cjs.js", + "import": "./dist/lite/pipelines.browser.esm2017.js" + }, + "default": "./dist/lite/pipelines.browser.esm2017.js" + }, + "./pipelines": { + "types": "./pipelines/pipelines.d.ts", + "node": { + "require": "./dist/pipelines.node.cjs.js", + "import": "./dist/pipelines.node.mjs" + }, + "react-native": "./dist/index.rn.esm2017.js", + "browser": { + "require": "./dist/pipelines.cjs.js", + "import": "./dist/pipelines.esm2017.js" + }, + "default": "./dist/pipelines.esm2017.js" + }, "./package.json": "./package.json" }, - "main": "dist/index.node.cjs.js", - "main-esm": "dist/index.node.mjs", + "main": "dist/node-cjs/index.node.cjs.js", + "main-esm": "dist/node-esm/index.node.mjs", "react-native": "dist/index.rn.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/browser-esm2017/index.esm2017.js", + "module": "dist/browser-esm2017/index.esm2017.js", "license": "Apache-2.0", "files": [ "dist", @@ -140,7 +168,7 @@ "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, - "typings": "dist/firestore/src/index.d.ts", + "types": "dist/index.d.ts", "nyc": { "extension": [ ".ts" diff --git a/packages/firestore/pipelines/package.json b/packages/firestore/pipelines/package.json new file mode 100644 index 00000000000..aab036bfdb0 --- /dev/null +++ b/packages/firestore/pipelines/package.json @@ -0,0 +1,14 @@ +{ + "name": "@firebase/firestore/pipelines", + "description": "pipelines", + "main": "../dist/pipelines.node.cjs.js", + "main-esm": "../dist/pipelines.node.mjs", + "module": "../dist/pipelines.browser.esm2017.js", + "browser": "../dist/pipelines.browser.esm2017.js", + "react-native": "../dist/pipelines.rn.esm2017.js", + "typings": "./pipelines.d.ts", + "private": true, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/firestore/pipelines/pipelines.d.ts b/packages/firestore/pipelines/pipelines.d.ts new file mode 100644 index 00000000000..e7edb233991 --- /dev/null +++ b/packages/firestore/pipelines/pipelines.d.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PipelineSource, Pipeline } from '../dist/pipelines'; + +// Augument the Firestore and Query classes with the pipeline() method. +// This is stripped from dist/lite/pipelines.d.ts during the build +// so it needs to be re-added here. +declare module '@firebase/firestore' { + interface Firestore { + pipeline(): PipelineSource; + } +} + +export * from '../dist/pipelines'; diff --git a/packages/firestore/pipelines/pipelines.node.ts b/packages/firestore/pipelines/pipelines.node.ts new file mode 100644 index 00000000000..fc0e91de0fc --- /dev/null +++ b/packages/firestore/pipelines/pipelines.node.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../src/api_pipelines'; diff --git a/packages/firestore/pipelines/pipelines.rn.ts b/packages/firestore/pipelines/pipelines.rn.ts new file mode 100644 index 00000000000..d5d4597190d --- /dev/null +++ b/packages/firestore/pipelines/pipelines.rn.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../src/api_pipelines'; diff --git a/packages/firestore/pipelines/pipelines.ts b/packages/firestore/pipelines/pipelines.ts new file mode 100644 index 00000000000..b056059adf4 --- /dev/null +++ b/packages/firestore/pipelines/pipelines.ts @@ -0,0 +1,51 @@ +/** + * Cloud Firestore + * + * @packageDocumentation + */ + +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// External exports: ./api +// These external exports will be stripped from the dist/pipelines.d.ts file +// by the prune-dts script, in order to reduce type duplication. However, these +// types need to be exported here to ensure that api-extractor behaves +// correctly. If a type from api.ts is missing from this export, then +// api-extractor may rename it with a suffix `_#`, e.g. `YourType_2`. +export type { + Timestamp, + DocumentReference, + VectorValue, + GeoPoint, + FieldPath, + DocumentData, + Query, + Firestore, + FirestoreDataConverter, + WithFieldValue, + PartialWithFieldValue, + SetOptions, + QueryDocumentSnapshot, + SnapshotOptions, + Primitive, + FieldValue, + SnapshotMetadata, + Bytes +} from '../src/api'; + +export * from '../src/api_pipelines'; diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index f9a29bef742..72758be2618 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -64,9 +64,11 @@ const allBuilds = [ // this is an intermediate build used to generate the actual esm and cjs builds // which add build target reporting { - input: './src/index.node.ts', + input: ['./src/index.node.ts', './pipelines/pipelines.node.ts'], output: { - file: pkg['main-esm'], + dir: 'dist/intermediate', + entryFileNames: '[name].mjs', + chunkFileNames: 'common-[hash].node.mjs', format: 'es', sourcemap: true }, @@ -79,9 +81,14 @@ const allBuilds = [ }, // Node CJS build { - input: pkg['main-esm'], + input: [ + 'dist/intermediate/index.node.mjs', + 'dist/intermediate/pipelines.node.mjs' + ], output: { - file: pkg.main, + dir: 'dist/', + entryFileNames: '[name].cjs.js', + chunkFileNames: 'common-[hash].node.cjs.js', format: 'cjs', sourcemap: true }, @@ -106,9 +113,14 @@ const allBuilds = [ }, // Node ESM build with build target reporting { - input: pkg['main-esm'], + input: [ + 'dist/intermediate/index.node.mjs', + 'dist/intermediate/pipelines.node.mjs' + ], output: { - file: pkg['main-esm'], + dir: 'dist/', + entryFileNames: '[name].mjs', + chunkFileNames: 'common-[hash].node.mjs', format: 'es', sourcemap: true }, @@ -125,9 +137,11 @@ const allBuilds = [ // this is an intermediate build used to generate the actual esm and cjs builds // which add build target reporting { - input: './src/index.ts', + input: ['./src/index.ts', './pipelines/pipelines.ts'], output: { - file: pkg.browser, + dir: 'dist/intermediate', + entryFileNames: '[name].js', + chunkFileNames: 'common-[hash].js', format: 'es', sourcemap: true }, @@ -139,10 +153,12 @@ const allBuilds = [ }, // Convert es2017 build to cjs { - input: pkg['browser'], + input: ['dist/intermediate/index.js', 'dist/intermediate/pipelines.js'], output: [ { - file: './dist/index.cjs.js', + dir: 'dist/', + entryFileNames: '[name].cjs.js', + chunkFileNames: 'common-[hash].cjs.js', format: 'cjs', sourcemap: true } @@ -158,10 +174,12 @@ const allBuilds = [ }, // es2017 build with build target reporting { - input: pkg['browser'], + input: ['dist/intermediate/index.js', 'dist/intermediate/pipelines.js'], output: [ { - file: pkg['browser'], + dir: 'dist/', + entryFileNames: '[name].esm2017.js', + chunkFileNames: 'common-[hash].esm2017.js', format: 'es', sourcemap: true } @@ -177,9 +195,11 @@ const allBuilds = [ }, // RN build { - input: './src/index.rn.ts', + input: ['./src/index.rn.ts', './pipelines/pipelines.rn.ts'], output: { - file: pkg['react-native'], + dir: 'dist/', + entryFileNames: '[name].js', + chunkFileNames: 'common-[hash].rn.js', format: 'es', sourcemap: true }, diff --git a/packages/firestore/rollup.config.lite.js b/packages/firestore/rollup.config.lite.js index 9ff4d57a8d8..00873e31fb4 100644 --- a/packages/firestore/rollup.config.lite.js +++ b/packages/firestore/rollup.config.lite.js @@ -56,9 +56,11 @@ const allBuilds = [ // this is an intermediate build used to generate the actual esm and cjs builds // which add build target reporting { - input: './lite/index.ts', + input: ['./lite/index.ts', './lite/pipelines/pipelines.ts'], output: { - file: path.resolve('./lite', pkg['main-esm']), + dir: 'dist/intermediate/lite/', + entryFileNames: '[name].node.mjs', + chunkFileNames: 'common-[hash].node.mjs', format: 'es', sourcemap: true }, @@ -77,9 +79,14 @@ const allBuilds = [ }, // Node CJS build { - input: path.resolve('./lite', pkg['main-esm']), + input: [ + 'dist/intermediate/lite/index.node.mjs', + 'dist/intermediate/lite/pipelines.node.mjs' + ], output: { - file: path.resolve('./lite', pkg.main), + dir: 'dist/lite/', + entryFileNames: '[name].cjs.js', + chunkFileNames: 'common-[hash].node.cjs.js', format: 'cjs', sourcemap: true }, @@ -102,9 +109,14 @@ const allBuilds = [ }, // Node ESM build { - input: path.resolve('./lite', pkg['main-esm']), + input: [ + 'dist/intermediate/lite/index.node.mjs', + 'dist/intermediate/lite/pipelines.node.mjs' + ], output: { - file: path.resolve('./lite', pkg['main-esm']), + dir: 'dist/lite/', + entryFileNames: '[name].mjs', + chunkFileNames: 'common-[hash].node.mjs', format: 'es', sourcemap: true }, @@ -121,9 +133,11 @@ const allBuilds = [ // this is an intermediate build used to generate the actual esm and cjs builds // which add build target reporting { - input: './lite/index.ts', + input: ['./lite/index.ts', './lite/pipelines/pipelines.ts'], output: { - file: path.resolve('./lite', pkg.browser), + dir: 'dist/intermediate/lite/', + entryFileNames: '[name].browser.js', + chunkFileNames: 'common-[hash].browser.js', format: 'es', sourcemap: true }, @@ -142,10 +156,15 @@ const allBuilds = [ }, // Convert es2017 build to CJS { - input: path.resolve('./lite', pkg.browser), + input: [ + 'dist/intermediate/lite/index.browser.js', + 'dist/intermediate/lite/pipelines.browser.js' + ], output: [ { - file: './dist/lite/index.cjs.js', + dir: 'dist/lite/', + entryFileNames: '[name].cjs.js', + chunkFileNames: 'common-[hash].cjs.js', format: 'es', sourcemap: true } @@ -161,10 +180,15 @@ const allBuilds = [ }, // Browser es2017 build { - input: path.resolve('./lite', pkg.browser), + input: [ + 'dist/intermediate/lite/index.browser.js', + 'dist/intermediate/lite/pipelines.browser.js' + ], output: [ { - file: path.resolve('./lite', pkg.browser), + dir: 'dist/lite/', + entryFileNames: '[name].esm2017.js', + chunkFileNames: 'common-[hash].esm2017.js', format: 'es', sourcemap: true } @@ -180,9 +204,11 @@ const allBuilds = [ }, // RN build { - input: './lite/index.ts', + input: ['./lite/index.ts', './lite/pipelines/pipelines.ts'], output: { - file: path.resolve('./lite', pkg['react-native']), + dir: 'dist/lite/', + entryFileNames: '[name].rn.esm2017.js', + chunkFileNames: 'common-[hash].rn.esm2017.js', format: 'es', sourcemap: true }, diff --git a/packages/firestore/src/api/aggregate.ts b/packages/firestore/src/api/aggregate.ts index f0e2c1e1dc0..453f9e0a841 100644 --- a/packages/firestore/src/api/aggregate.ts +++ b/packages/firestore/src/api/aggregate.ts @@ -15,17 +15,21 @@ * limitations under the License. */ -import { AggregateField, AggregateSpec, DocumentData, Query } from '../api'; import { AggregateImpl } from '../core/aggregate'; import { firestoreClientRunAggregateQuery } from '../core/firestore_client'; import { count } from '../lite-api/aggregate'; -import { AggregateQuerySnapshot } from '../lite-api/aggregate_types'; +import { + AggregateField, + AggregateQuerySnapshot, + AggregateSpec +} from '../lite-api/aggregate_types'; +import { DocumentData, Query } from '../lite-api/reference'; import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; import { cast } from '../util/input_validation'; import { mapToArray } from '../util/obj'; import { ensureFirestoreConfigured, Firestore } from './database'; -import { ExpUserDataWriter } from './reference_impl'; +import { ExpUserDataWriter } from './user_data_writer'; export { aggregateQuerySnapshotEqual, diff --git a/packages/firestore/src/api/parse_context.ts b/packages/firestore/src/api/parse_context.ts index ce3c221f66e..2381bcff4cd 100644 --- a/packages/firestore/src/api/parse_context.ts +++ b/packages/firestore/src/api/parse_context.ts @@ -16,8 +16,53 @@ */ import { DatabaseId } from '../core/database_info'; +import { UserDataSource } from '../lite-api/user_data_reader'; +import { DocumentKey } from '../model/document_key'; +import { FieldTransform } from '../model/mutation'; +import { FieldPath as InternalFieldPath } from '../model/path'; +import { JsonProtoSerializer } from '../remote/serializer'; +import { FirestoreError } from '../util/error'; + +/** Contains the settings that are mutated as we parse user data. */ +export interface ContextSettings { + /** Indicates what kind of API method this data came from. */ + readonly dataSource: UserDataSource; + /** The name of the method the user called to create the ParseContext. */ + readonly methodName: string; + /** The document the user is attempting to modify, if that applies. */ + readonly targetDoc?: DocumentKey; + /** + * A path within the object being parsed. This could be an empty path (in + * which case the context represents the root of the data being parsed), or a + * nonempty path (indicating the context represents a nested location within + * the data). + */ + readonly path?: InternalFieldPath; + /** + * Whether or not this context corresponds to an element of an array. + * If not set, elements are treated as if they were outside of arrays. + */ + readonly arrayElement?: boolean; + /** + * Whether or not a converter was specified in this context. If true, error + * messages will reference the converter when invalid data is provided. + */ + readonly hasConverter?: boolean; +} export interface ParseContext { + readonly settings: ContextSettings; readonly databaseId: DatabaseId; + readonly serializer: JsonProtoSerializer; readonly ignoreUndefinedProperties: boolean; + fieldTransforms: FieldTransform[]; + fieldMask: InternalFieldPath[]; + get path(): InternalFieldPath | undefined; + get dataSource(): UserDataSource; + contextWith(configuration: Partial): ParseContext; + childContextForField(field: string): ParseContext; + childContextForFieldPath(field: InternalFieldPath): ParseContext; + childContextForArray(index: number): ParseContext; + createError(reason: string): FirestoreError; + contains(fieldPath: InternalFieldPath): boolean; } diff --git a/packages/firestore/src/api/pipeline.ts b/packages/firestore/src/api/pipeline.ts new file mode 100644 index 00000000000..e7cd6e875eb --- /dev/null +++ b/packages/firestore/src/api/pipeline.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pipeline as LitePipeline } from '../lite-api/pipeline'; +import { Stage } from '../lite-api/stage'; +import { UserDataReader } from '../lite-api/user_data_reader'; +import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; + +import { Firestore } from './database'; + +export class Pipeline extends LitePipeline { + /** + * @internal + * @private + * @param db + * @param userDataReader + * @param userDataWriter + * @param stages + * @param converter + * @protected + */ + protected newPipeline( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + stages: Stage[] + ): Pipeline { + return new Pipeline(db, userDataReader, userDataWriter, stages); + } +} diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts new file mode 100644 index 00000000000..ba6e08105bb --- /dev/null +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pipeline } from '../api/pipeline'; +import { firestoreClientExecutePipeline } from '../core/firestore_client'; +import { Pipeline as LitePipeline } from '../lite-api/pipeline'; +import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; +import { PipelineSource } from '../lite-api/pipeline-source'; +import { Stage } from '../lite-api/stage'; +import { newUserDataReader } from '../lite-api/user_data_reader'; +import { cast } from '../util/input_validation'; + +import { ensureFirestoreConfigured, Firestore } from './database'; +import { DocumentReference } from './reference'; +import { ExpUserDataWriter } from './user_data_writer'; + +declare module './database' { + interface Firestore { + pipeline(): PipelineSource; + } +} + +/** + * Executes this pipeline and returns a Promise to represent the asynchronous operation. + * + * The returned Promise can be used to track the progress of the pipeline execution + * and retrieve the results (or handle any errors) asynchronously. + * + * The pipeline results are returned as a {@link PipelineSnapshot} that contains + * a list of {@link PipelineResult} objects. Each {@link PipelineResult} typically + * represents a single key/value map that has passed through all the + * stages of the pipeline, however this might differ depending on the stages involved in the + * pipeline. For example: + * + *
    + *
  • If there are no stages or only transformation stages, each {@link PipelineResult} + * represents a single document.
  • + *
  • If there is an aggregation, only a single {@link PipelineResult} is returned, + * representing the aggregated results over the entire dataset .
  • + *
  • If there is an aggregation stage with grouping, each {@link PipelineResult} represents a + * distinct group and its associated aggregated values.
  • + *
+ * + *

Example: + * + * ```typescript + * const snapshot: PipelineSnapshot = await execute(firestore.pipeline().collection("books") + * .where(gt(field("rating"), 4.5)) + * .select("title", "author", "rating")); + * + * const results: PipelineResults = snapshot.results; + * ``` + * + * @param pipeline The pipeline to execute. + * @return A Promise representing the asynchronous pipeline execution. + */ +export function execute(pipeline: LitePipeline): Promise { + const firestore = cast(pipeline._db, Firestore); + const client = ensureFirestoreConfigured(firestore); + return firestoreClientExecutePipeline(client, pipeline).then(result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; + + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.key?.path + ? new DocumentReference(firestore, null, element.key) + : undefined, + element.fields, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) + ); + + return new PipelineSnapshot(pipeline, docs, executionTime); + }); +} + +// Augment the Firestore class with the pipeline() factory method +Firestore.prototype.pipeline = function (): PipelineSource { + return new PipelineSource(this._databaseId, (stages: Stage[]) => { + return new Pipeline( + this, + newUserDataReader(this), + new ExpUserDataWriter(this), + stages + ); + }); +}; diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index 8fa21a13e6d..4283453d81d 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -37,7 +37,6 @@ import { } from '../core/firestore_client'; import { newQueryForPath, Query as InternalQuery } from '../core/query'; import { ViewSnapshot } from '../core/view_snapshot'; -import { Bytes } from '../lite-api/bytes'; import { FieldPath } from '../lite-api/field_path'; import { validateHasExplicitOrderByForLimitToLast } from '../lite-api/query'; import { @@ -59,11 +58,9 @@ import { parseUpdateData, parseUpdateVarargs } from '../lite-api/user_data_reader'; -import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { DocumentKey } from '../model/document_key'; import { DeleteMutation, Mutation, Precondition } from '../model/mutation'; import { debugAssert } from '../util/assert'; -import { ByteString } from '../util/byte_string'; import { Code, FirestoreError } from '../util/error'; import { cast } from '../util/input_validation'; @@ -74,6 +71,7 @@ import { QuerySnapshot, SnapshotMetadata } from './snapshot'; +import { ExpUserDataWriter } from './user_data_writer'; /** * An options object that can be passed to {@link (onSnapshot:1)} and {@link @@ -130,21 +128,6 @@ export function getDoc( ).then(snapshot => convertToDocSnapshot(firestore, reference, snapshot)); } -export class ExpUserDataWriter extends AbstractUserDataWriter { - constructor(protected firestore: Firestore) { - super(); - } - - protected convertBytes(bytes: ByteString): Bytes { - return new Bytes(bytes); - } - - protected convertReference(name: string): DocumentReference { - const key = this.convertDocumentKey(name, this.firestore._databaseId); - return new DocumentReference(this.firestore, /* converter= */ null, key); - } -} - /** * Reads the document referred to by this `DocumentReference` from cache. * Returns an error if the document is not currently cached. diff --git a/packages/firestore/src/api/transaction.ts b/packages/firestore/src/api/transaction.ts index 955866f19b4..8f83f527182 100644 --- a/packages/firestore/src/api/transaction.ts +++ b/packages/firestore/src/api/transaction.ts @@ -28,9 +28,9 @@ import { validateReference } from '../lite-api/write_batch'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; -import { ExpUserDataWriter } from './reference_impl'; import { DocumentSnapshot, SnapshotMetadata } from './snapshot'; import { TransactionOptions } from './transaction_options'; +import { ExpUserDataWriter } from './user_data_writer'; /** * A reference to a transaction. diff --git a/packages/firestore/src/api/user_data_writer.ts b/packages/firestore/src/api/user_data_writer.ts new file mode 100644 index 00000000000..3567f72cd93 --- /dev/null +++ b/packages/firestore/src/api/user_data_writer.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Bytes } from '../lite-api/bytes'; +import { DocumentReference } from '../lite-api/reference'; +import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; +import { ByteString } from '../util/byte_string'; + +import { Firestore } from './database'; + +export class ExpUserDataWriter extends AbstractUserDataWriter { + constructor(protected firestore: Firestore) { + super(); + } + + protected convertBytes(bytes: ByteString): Bytes { + return new Bytes(bytes); + } + + protected convertReference(name: string): DocumentReference { + const key = this.convertDocumentKey(name, this.firestore._databaseId); + return new DocumentReference(this.firestore, /* converter= */ null, key); + } +} diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts new file mode 100644 index 00000000000..b632025f374 --- /dev/null +++ b/packages/firestore/src/api_pipelines.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { PipelineSource } from './lite-api/pipeline-source'; + +export { + PipelineResult, + PipelineSnapshot, + pipelineResultEqual +} from './lite-api/pipeline-result'; + +export { Pipeline } from './api/pipeline'; + +export { execute } from './api/pipeline_impl'; + +export { + Stage, + FindNearestOptions, + AddFields, + Aggregate, + Distinct, + CollectionSource, + CollectionGroupSource, + DatabaseSource, + DocumentsSource, + Where, + FindNearest, + Limit, + Offset, + Select, + Sort, + GenericStage +} from './lite-api/stage'; + +export { + field, + constant, + add, + subtract, + multiply, + divide, + mod, + eq, + neq, + lt, + lte, + gt, + gte, + arrayConcat, + arrayContains, + arrayContainsAny, + arrayContainsAll, + arrayLength, + eqAny, + notEqAny, + xor, + cond, + not, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + replaceFirst, + replaceAll, + byteLength, + charLength, + like, + regexContains, + regexMatch, + strContains, + startsWith, + endsWith, + toLower, + toUpper, + trim, + strConcat, + mapGet, + countAll, + count, + sum, + avg, + and, + or, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSub, + ascending, + descending, + countIf, + bitAnd, + bitOr, + bitXor, + bitNot, + bitLeftShift, + bitRightShift, + rand, + array, + arrayOffset, + isError, + ifError, + isAbsent, + isNull, + isNotNull, + isNotNan, + map, + mapRemove, + mapMerge, + documentId, + substr, + Expr, + ExprWithAlias, + Field, + Constant, + FunctionExpr, + Ordering +} from './lite-api/expressions'; + +export type { + ExprType, + AggregateWithAlias, + Selectable, + BooleanExpr, + AggregateFunction +} from './lite-api/expressions'; + +export { _internalPipelineToExecutePipelineRequestProto } from './remote/internal_serializer'; diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index d04f37a3d7b..7b5d05eb93d 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -23,6 +23,7 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; +import { Pipeline } from '../lite-api/pipeline'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, @@ -38,11 +39,16 @@ import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { FieldIndex } from '../model/field_index'; import { Mutation } from '../model/mutation'; +import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { toByteStreamReader } from '../platform/byte_stream_reader'; import { newSerializer } from '../platform/serializer'; import { newTextEncoder } from '../platform/text_serializer'; import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; -import { Datastore, invokeRunAggregationQueryRpc } from '../remote/datastore'; +import { + Datastore, + invokeExecutePipeline, + invokeRunAggregationQueryRpc +} from '../remote/datastore'; import { RemoteStore, remoteStoreDisableNetwork, @@ -562,6 +568,23 @@ export function firestoreClientRunAggregateQuery( return deferred.promise; } +export function firestoreClientExecutePipeline( + client: FirestoreClient, + pipeline: Pipeline +): Promise { + const deferred = new Deferred(); + + client.asyncQueue.enqueueAndForget(async () => { + try { + const datastore = await getDatastore(client); + deferred.resolve(invokeExecutePipeline(datastore, pipeline)); + } catch (e) { + deferred.reject(e as Error); + } + }); + return deferred.promise; +} + export function firestoreClientWrite( client: FirestoreClient, mutations: Mutation[] diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts new file mode 100644 index 00000000000..cb3342eca4e --- /dev/null +++ b/packages/firestore/src/core/pipeline-util.ts @@ -0,0 +1,282 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Firestore } from '../lite-api/database'; +import { + Constant, + BooleanExpr, + and, + or, + Ordering, + lt, + gt, + field +} from '../lite-api/expressions'; +import { Pipeline } from '../lite-api/pipeline'; +import { doc } from '../lite-api/reference'; +import { isNanValue, isNullValue } from '../model/values'; +import { fail } from '../util/assert'; + +import { Bound } from './bound'; +import { + CompositeFilter as CompositeFilterInternal, + CompositeOperator, + FieldFilter as FieldFilterInternal, + Filter as FilterInternal, + Operator +} from './filter'; +import { Direction } from './order_by'; +import { + isCollectionGroupQuery, + isDocumentQuery, + LimitType, + Query, + queryNormalizedOrderBy +} from './query'; + +/* eslint @typescript-eslint/no-explicit-any: 0 */ + +export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { + if (f instanceof FieldFilterInternal) { + const fieldValue = field(f.field.toString()); + if (isNanValue(f.value)) { + if (f.op === Operator.EQUAL) { + return and(fieldValue.exists(), fieldValue.isNan()); + } else { + return and(fieldValue.exists(), fieldValue.isNotNan()); + } + } else if (isNullValue(f.value)) { + if (f.op === Operator.EQUAL) { + return and(fieldValue.exists(), fieldValue.isNull()); + } else { + return and(fieldValue.exists(), fieldValue.isNotNull()); + } + } else { + // Comparison filters + const value = f.value; + switch (f.op) { + case Operator.LESS_THAN: + return and( + fieldValue.exists(), + fieldValue.lt(Constant._fromProto(value)) + ); + case Operator.LESS_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.lte(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN: + return and( + fieldValue.exists(), + fieldValue.gt(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.gte(Constant._fromProto(value)) + ); + case Operator.EQUAL: + return and( + fieldValue.exists(), + fieldValue.eq(Constant._fromProto(value)) + ); + case Operator.NOT_EQUAL: + return and( + fieldValue.exists(), + fieldValue.neq(Constant._fromProto(value)) + ); + case Operator.ARRAY_CONTAINS: + return and( + fieldValue.exists(), + fieldValue.arrayContains(Constant._fromProto(value)) + ); + case Operator.IN: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + if (!values) { + return and(fieldValue.exists(), fieldValue.eqAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.eq(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.eqAny(values)); + } + } + case Operator.ARRAY_CONTAINS_ANY: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + return and(fieldValue.exists(), fieldValue.arrayContainsAny(values!)); + } + case Operator.NOT_IN: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + if (!values) { + return and(fieldValue.exists(), fieldValue.notEqAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.neq(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.notEqAny(values)); + } + } + default: + fail(0x9047, 'Unexpected operator'); + } + } + } else if (f instanceof CompositeFilterInternal) { + switch (f.op) { + case CompositeOperator.AND: { + const conditions = f.getFilters().map(f => toPipelineBooleanExpr(f)); + return and(conditions[0], conditions[1], ...conditions.slice(2)); + } + case CompositeOperator.OR: { + const conditions = f.getFilters().map(f => toPipelineBooleanExpr(f)); + return or(conditions[0], conditions[1], ...conditions.slice(2)); + } + default: + fail(0x89ea, 'Unexpected operator'); + } + } + + throw new Error(`Failed to convert filter to pipeline conditions: ${f}`); +} + +function reverseOrderings(orderings: Ordering[]): Ordering[] { + return orderings.map( + o => + new Ordering( + o.expr, + o.direction === 'ascending' ? 'descending' : 'ascending' + ) + ); +} + +export function toPipeline(query: Query, db: Firestore): Pipeline { + let pipeline: Pipeline; + if (isCollectionGroupQuery(query)) { + pipeline = db.pipeline().collectionGroup(query.collectionGroup!); + } else if (isDocumentQuery(query)) { + pipeline = db.pipeline().documents([doc(db, query.path.canonicalString())]); + } else { + pipeline = db.pipeline().collection(query.path.canonicalString()); + } + + // filters + for (const filter of query.filters) { + pipeline = pipeline.where(toPipelineBooleanExpr(filter)); + } + + // orders + const orders = queryNormalizedOrderBy(query); + const existsConditions = orders.map(order => + field(order.field.canonicalString()).exists() + ); + if (existsConditions.length > 1) { + pipeline = pipeline.where( + and( + existsConditions[0], + existsConditions[1], + ...existsConditions.slice(2) + ) + ); + } else { + pipeline = pipeline.where(existsConditions[0]); + } + + const orderings = orders.map(order => + order.dir === Direction.ASCENDING + ? field(order.field.canonicalString()).ascending() + : field(order.field.canonicalString()).descending() + ); + + if (orderings.length > 0) { + if (query.limitType === LimitType.Last) { + const actualOrderings = reverseOrderings(orderings); + pipeline = pipeline.sort(actualOrderings[0], ...actualOrderings.slice(1)); + // cursors + if (query.startAt !== null) { + pipeline = pipeline.where( + whereConditionsFromCursor(query.startAt, orderings, 'after') + ); + } + + if (query.endAt !== null) { + pipeline = pipeline.where( + whereConditionsFromCursor(query.endAt, orderings, 'before') + ); + } + + pipeline = pipeline.limit(query.limit!); + pipeline = pipeline.sort(orderings[0], ...orderings.slice(1)); + } else { + pipeline = pipeline.sort(orderings[0], ...orderings.slice(1)); + if (query.startAt !== null) { + pipeline = pipeline.where( + whereConditionsFromCursor(query.startAt, orderings, 'after') + ); + } + if (query.endAt !== null) { + pipeline = pipeline.where( + whereConditionsFromCursor(query.endAt, orderings, 'before') + ); + } + + if (query.limit !== null) { + pipeline = pipeline.limit(query.limit); + } + } + } + + return pipeline; +} + +function whereConditionsFromCursor( + bound: Bound, + orderings: Ordering[], + position: 'before' | 'after' +): BooleanExpr { + // The filterFunc is either greater than or less than + const filterFunc = position === 'before' ? lt : gt; + const cursors = bound.position.map(value => Constant._fromProto(value)); + const size = cursors.length; + + let field = orderings[size - 1].expr; + let value = cursors[size - 1]; + + // Add condition for last bound + let condition: BooleanExpr = filterFunc(field, value); + if (bound.inclusive) { + // When the cursor bound is inclusive, then the last bound + // can be equal to the value, otherwise it's not equal + condition = or(condition, field.eq(value)); + } + + // Iterate backwards over the remaining bounds, adding + // a condition for each one + for (let i = size - 2; i >= 0; i--) { + field = orderings[i].expr; + value = cursors[i]; + + // For each field in the orderings, the condition is either + // a) lt|gt the cursor value, + // b) or equal the cursor value and lt|gt the cursor values for other fields + condition = or(filterFunc(field, value), and(field.eq(value), condition)); + } + + return condition; +} diff --git a/packages/firestore/src/lite-api/database_augmentation.ts b/packages/firestore/src/lite-api/database_augmentation.ts new file mode 100644 index 00000000000..bf25e9c59c8 --- /dev/null +++ b/packages/firestore/src/lite-api/database_augmentation.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts new file mode 100644 index 00000000000..fa0fbf68064 --- /dev/null +++ b/packages/firestore/src/lite-api/expressions.ts @@ -0,0 +1,6905 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ParseContext } from '../api/parse_context'; +import { + DOCUMENT_KEY_NAME, + FieldPath as InternalFieldPath +} from '../model/path'; +import { Value as ProtoValue } from '../protos/firestore_proto_api'; +import { + JsonProtoSerializer, + ProtoValueSerializable, + toMapValue, + toStringValue, + UserData +} from '../remote/serializer'; +import { hardAssert } from '../util/assert'; +import { isPlainObject } from '../util/input_validation'; +import { isFirestoreValue } from '../util/proto'; +import { isString } from '../util/types'; + +import { Bytes } from './bytes'; +import { documentId as documentIdFieldPath, FieldPath } from './field_path'; +import { GeoPoint } from './geo_point'; +import { DocumentReference } from './reference'; +import { Timestamp } from './timestamp'; +import { + fieldPathFromArgument, + parseData, + UserDataReader, + UserDataSource +} from './user_data_reader'; +import { VectorValue } from './vector_value'; + +/** + * @beta + * + * An enumeration of the different types of expressions. + */ +export type ExprType = + | 'Field' + | 'Constant' + | 'Function' + | 'AggregateFunction' + | 'ListOfExprs' + | 'ExprWithAlias'; + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +function valueToDefaultExpr(value: unknown): Expr { + let result: Expr | undefined; + if (value instanceof Expr) { + return value; + } else if (isPlainObject(value)) { + result = map(value as Record); + } else if (value instanceof Array) { + result = array(value); + } else { + result = new Constant(value); + } + + result._createdFromLiteral = true; + return result; +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +function vectorToExpr(value: VectorValue | number[] | Expr): Expr { + if (value instanceof Expr) { + return value; + } else { + const result = constantVector(value); + result._createdFromLiteral = true; + return result; + } +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * If the input is a string, it is assumed to be a field name, and a + * field(value) is returned. + * + * @private + * @internal + * @param value + */ +function fieldOrExpression(value: unknown): Expr { + if (isString(value)) { + const result = field(value); + result._createdFromLiteral = true; + return result; + } else { + return valueToDefaultExpr(value); + } +} + +/** + * @beta + * + * Represents an expression that can be evaluated to a value within the execution of a {@link + * Pipeline}. + * + * Expressions are the building blocks for creating complex queries and transformations in + * Firestore pipelines. They can represent: + * + * - **Field references:** Access values from document fields. + * - **Literals:** Represent constant values (strings, numbers, booleans). + * - **Function calls:** Apply functions to one or more expressions. + * + * The `Expr` class provides a fluent API for building expressions. You can chain together + * method calls to create complex expressions. + */ +export abstract class Expr implements ProtoValueSerializable, UserData { + abstract readonly exprType: ExprType; + + /** + * @internal + * @private + * Indicates if this expression was created from a literal value passed + * by the caller. + */ + _createdFromLiteral: boolean = false; + + /** + * @private + * @internal + */ + abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; + _protoValueType = 'ProtoValue' as const; + + /** + * @private + * @internal + */ + abstract _readUserData( + dataReader: UserDataReader, + context?: ParseContext + ): void; + + /** + * Creates an expression that adds this expression to another expression. + * + * ```typescript + * // Add the value of the 'quantity' field and the 'reserve' field. + * field("quantity").add(field("reserve")); + * ``` + * + * @param second The expression or literal to add to this expression. + * @param others Optional additional expressions or literals to add to this expression. + * @return A new `Expr` representing the addition operation. + */ + add(second: Expr | unknown, ...others: Array): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('add', [ + this, + ...values.map(value => valueToDefaultExpr(value)) + ]); + } + + /** + * Creates an expression that subtracts another expression from this expression. + * + * ```typescript + * // Subtract the 'discount' field from the 'price' field + * field("price").subtract(field("discount")); + * ``` + * + * @param other The expression to subtract from this expression. + * @return A new `Expr` representing the subtraction operation. + */ + subtract(other: Expr): FunctionExpr; + + /** + * Creates an expression that subtracts a constant value from this expression. + * + * ```typescript + * // Subtract 20 from the value of the 'total' field + * field("total").subtract(20); + * ``` + * + * @param other The constant value to subtract. + * @return A new `Expr` representing the subtraction operation. + */ + subtract(other: number): FunctionExpr; + subtract(other: number | Expr): FunctionExpr { + return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that multiplies this expression by another expression. + * + * ```typescript + * // Multiply the 'quantity' field by the 'price' field + * field("quantity").multiply(field("price")); + * ``` + * + * @param second The second expression or literal to multiply by. + * @param others Optional additional expressions or literals to multiply by. + * @return A new `Expr` representing the multiplication operation. + */ + multiply( + second: Expr | number, + ...others: Array + ): FunctionExpr { + return new FunctionExpr('multiply', [ + this, + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ]); + } + + /** + * Creates an expression that divides this expression by another expression. + * + * ```typescript + * // Divide the 'total' field by the 'count' field + * field("total").divide(field("count")); + * ``` + * + * @param other The expression to divide by. + * @return A new `Expr` representing the division operation. + */ + divide(other: Expr): FunctionExpr; + + /** + * Creates an expression that divides this expression by a constant value. + * + * ```typescript + * // Divide the 'value' field by 10 + * field("value").divide(10); + * ``` + * + * @param other The constant value to divide by. + * @return A new `Expr` representing the division operation. + */ + divide(other: number): FunctionExpr; + divide(other: number | Expr): FunctionExpr { + return new FunctionExpr('divide', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing this expression by another expression. + * + * ```typescript + * // Calculate the remainder of dividing the 'value' field by the 'divisor' field + * field("value").mod(field("divisor")); + * ``` + * + * @param expression The expression to divide by. + * @return A new `Expr` representing the modulo operation. + */ + mod(expression: Expr): FunctionExpr; + + /** + * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. + * + * ```typescript + * // Calculate the remainder of dividing the 'value' field by 10 + * field("value").mod(10); + * ``` + * + * @param value The constant value to divide by. + * @return A new `Expr` representing the modulo operation. + */ + mod(value: number): FunctionExpr; + mod(other: number | Expr): FunctionExpr { + return new FunctionExpr('mod', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is equal to another expression. + * + * ```typescript + * // Check if the 'age' field is equal to 21 + * field("age").eq(21); + * ``` + * + * @param expression The expression to compare for equality. + * @return A new `Expr` representing the equality comparison. + */ + eq(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is equal to a constant value. + * + * ```typescript + * // Check if the 'city' field is equal to "London" + * field("city").eq("London"); + * ``` + * + * @param value The constant value to compare for equality. + * @return A new `Expr` representing the equality comparison. + */ + eq(value: unknown): BooleanExpr; + eq(other: unknown): BooleanExpr { + return new BooleanExpr('eq', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is not equal to another expression. + * + * ```typescript + * // Check if the 'status' field is not equal to "completed" + * field("status").neq("completed"); + * ``` + * + * @param expression The expression to compare for inequality. + * @return A new `Expr` representing the inequality comparison. + */ + neq(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is not equal to a constant value. + * + * ```typescript + * // Check if the 'country' field is not equal to "USA" + * field("country").neq("USA"); + * ``` + * + * @param value The constant value to compare for inequality. + * @return A new `Expr` representing the inequality comparison. + */ + neq(value: unknown): BooleanExpr; + neq(other: unknown): BooleanExpr { + return new BooleanExpr('neq', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is less than another expression. + * + * ```typescript + * // Check if the 'age' field is less than 'limit' + * field("age").lt(field('limit')); + * ``` + * + * @param experession The expression to compare for less than. + * @return A new `Expr` representing the less than comparison. + */ + lt(experession: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is less than a constant value. + * + * ```typescript + * // Check if the 'price' field is less than 50 + * field("price").lt(50); + * ``` + * + * @param value The constant value to compare for less than. + * @return A new `Expr` representing the less than comparison. + */ + lt(value: unknown): BooleanExpr; + lt(other: unknown): BooleanExpr { + return new BooleanExpr('lt', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is less than or equal to another + * expression. + * + * ```typescript + * // Check if the 'quantity' field is less than or equal to 20 + * field("quantity").lte(constant(20)); + * ``` + * + * @param expression The expression to compare for less than or equal to. + * @return A new `Expr` representing the less than or equal to comparison. + */ + lte(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is less than or equal to a constant value. + * + * ```typescript + * // Check if the 'score' field is less than or equal to 70 + * field("score").lte(70); + * ``` + * + * @param value The constant value to compare for less than or equal to. + * @return A new `Expr` representing the less than or equal to comparison. + */ + lte(value: unknown): BooleanExpr; + lte(other: unknown): BooleanExpr { + return new BooleanExpr('lte', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is greater than another expression. + * + * ```typescript + * // Check if the 'age' field is greater than the 'limit' field + * field("age").gt(field("limit")); + * ``` + * + * @param expression The expression to compare for greater than. + * @return A new `Expr` representing the greater than comparison. + */ + gt(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is greater than a constant value. + * + * ```typescript + * // Check if the 'price' field is greater than 100 + * field("price").gt(100); + * ``` + * + * @param value The constant value to compare for greater than. + * @return A new `Expr` representing the greater than comparison. + */ + gt(value: unknown): BooleanExpr; + gt(other: unknown): BooleanExpr { + return new BooleanExpr('gt', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that checks if this expression is greater than or equal to another + * expression. + * + * ```typescript + * // Check if the 'quantity' field is greater than or equal to field 'requirement' plus 1 + * field("quantity").gte(field('requirement').add(1)); + * ``` + * + * @param expression The expression to compare for greater than or equal to. + * @return A new `Expr` representing the greater than or equal to comparison. + */ + gte(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if this expression is greater than or equal to a constant + * value. + * + * ```typescript + * // Check if the 'score' field is greater than or equal to 80 + * field("score").gte(80); + * ``` + * + * @param value The constant value to compare for greater than or equal to. + * @return A new `Expr` representing the greater than or equal to comparison. + */ + gte(value: unknown): BooleanExpr; + gte(other: unknown): BooleanExpr { + return new BooleanExpr('gte', [this, valueToDefaultExpr(other)]); + } + + /** + * Creates an expression that concatenates an array expression with one or more other arrays. + * + * ```typescript + * // Combine the 'items' array with another array field. + * field("items").arrayConcat(field("otherItems")); + * ``` + * @param secondArray Second array expression or array literal to concatenate. + * @param otherArrays Optional additional array expressions or array literals to concatenate. + * @return A new `Expr` representing the concatenated array. + */ + arrayConcat( + secondArray: Expr | unknown[], + ...otherArrays: Array + ): FunctionExpr { + const elements = [secondArray, ...otherArrays]; + const exprValues = elements.map(value => valueToDefaultExpr(value)); + return new FunctionExpr('array_concat', [this, ...exprValues]); + } + + /** + * Creates an expression that checks if an array contains a specific element. + * + * ```typescript + * // Check if the 'sizes' array contains the value from the 'selectedSize' field + * field("sizes").arrayContains(field("selectedSize")); + * ``` + * + * @param expression The element to search for in the array. + * @return A new `Expr` representing the 'array_contains' comparison. + */ + arrayContains(expression: Expr): BooleanExpr; + + /** + * Creates an expression that checks if an array contains a specific value. + * + * ```typescript + * // Check if the 'colors' array contains "red" + * field("colors").arrayContains("red"); + * ``` + * + * @param value The element to search for in the array. + * @return A new `Expr` representing the 'array_contains' comparison. + */ + arrayContains(value: unknown): BooleanExpr; + arrayContains(element: unknown): BooleanExpr { + return new BooleanExpr('array_contains', [ + this, + valueToDefaultExpr(element) + ]); + } + + /** + * Creates an expression that checks if an array contains all the specified elements. + * + * ```typescript + * // Check if the 'tags' array contains both the value in field "tag1" and the literal value "tag2" + * field("tags").arrayContainsAll([field("tag1"), "tag2"]); + * ``` + * + * @param values The elements to check for in the array. + * @return A new `Expr` representing the 'array_contains_all' comparison. + */ + arrayContainsAll(values: Array): BooleanExpr; + + /** + * Creates an expression that checks if an array contains all the specified elements. + * + * ```typescript + * // Check if the 'tags' array contains both of the values from field "tag1" and the literal value "tag2" + * field("tags").arrayContainsAll(array([field("tag1"), "tag2"])); + * ``` + * + * @param arrayExpression The elements to check for in the array. + * @return A new `Expr` representing the 'array_contains_all' comparison. + */ + arrayContainsAll(arrayExpression: Expr): BooleanExpr; + arrayContainsAll(values: unknown[] | Expr): BooleanExpr { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr)) + : values; + return new BooleanExpr('array_contains_all', [this, normalizedExpr]); + } + + /** + * Creates an expression that checks if an array contains any of the specified elements. + * + * ```typescript + * // Check if the 'categories' array contains either values from field "cate1" or "cate2" + * field("categories").arrayContainsAny([field("cate1"), field("cate2")]); + * ``` + * + * @param values The elements to check for in the array. + * @return A new `Expr` representing the 'array_contains_any' comparison. + */ + arrayContainsAny(values: Array): BooleanExpr; + + /** + * Creates an expression that checks if an array contains any of the specified elements. + * + * ```typescript + * // Check if the 'groups' array contains either the value from the 'userGroup' field + * // or the value "guest" + * field("groups").arrayContainsAny(array([field("userGroup"), "guest"])); + * ``` + * + * @param arrayExpression The elements to check for in the array. + * @return A new `Expr` representing the 'array_contains_any' comparison. + */ + arrayContainsAny(arrayExpression: Expr): BooleanExpr; + arrayContainsAny(values: Array | Expr): BooleanExpr { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr)) + : values; + return new BooleanExpr('array_contains_any', [this, normalizedExpr]); + } + + /** + * Creates an expression that calculates the length of an array. + * + * ```typescript + * // Get the number of items in the 'cart' array + * field("cart").arrayLength(); + * ``` + * + * @return A new `Expr` representing the length of the array. + */ + arrayLength(): FunctionExpr { + return new FunctionExpr('array_length', [this]); + } + + /** + * Creates an expression that checks if this expression is equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' + * field("category").eqAny("Electronics", field("primaryType")); + * ``` + * + * @param values The values or expressions to check against. + * @return A new `Expr` representing the 'IN' comparison. + */ + eqAny(values: Array): BooleanExpr; + + /** + * Creates an expression that checks if this expression is equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' + * field("category").eqAny(array(["Electronics", field("primaryType")])); + * ``` + * + * @param arrayExpression An expression that evaluates to an array of values to check against. + * @return A new `Expr` representing the 'IN' comparison. + */ + eqAny(arrayExpression: Expr): BooleanExpr; + eqAny(others: unknown[] | Expr): BooleanExpr { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr)) + : others; + return new BooleanExpr('eq_any', [this, exprOthers]); + } + + /** + * Creates an expression that checks if this expression is not equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' + * field("status").notEqAny(["pending", field("rejectedStatus")]); + * ``` + * + * @param values The values or expressions to check against. + * @return A new `Expr` representing the 'NotEqAny' comparison. + */ + notEqAny(values: Array): BooleanExpr; + + /** + * Creates an expression that checks if this expression is not equal to any of the values in the evaluated expression. + * + * ```typescript + * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' + * field("status").notEqAny(field('rejectedStatuses')); + * ``` + * + * @param arrayExpression The values or expressions to check against. + * @return A new `Expr` representing the 'NotEqAny' comparison. + */ + notEqAny(arrayExpression: Expr): BooleanExpr; + notEqAny(others: unknown[] | Expr): BooleanExpr { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr)) + : others; + return new BooleanExpr('not_eq_any', [this, exprOthers]); + } + + /** + * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * field("value").divide(0).isNaN(); + * ``` + * + * @return A new `Expr` representing the 'isNaN' check. + */ + isNan(): BooleanExpr { + return new BooleanExpr('is_nan', [this]); + } + + /** + * Creates an expression that checks if this expression evaluates to 'Null'. + * + * ```typescript + * // Check if the result of a calculation is NaN + * field("value").isNull(); + * ``` + * + * @return A new `Expr` representing the 'isNull' check. + */ + isNull(): BooleanExpr { + return new BooleanExpr('is_null', [this]); + } + + /** + * Creates an expression that checks if a field exists in the document. + * + * ```typescript + * // Check if the document has a field named "phoneNumber" + * field("phoneNumber").exists(); + * ``` + * + * @return A new `Expr` representing the 'exists' check. + */ + exists(): BooleanExpr { + return new BooleanExpr('exists', [this]); + } + + /** + * Creates an expression that calculates the character length of a string in UTF-8. + * + * ```typescript + * // Get the character length of the 'name' field in its UTF-8 form. + * field("name").charLength(); + * ``` + * + * @return A new `Expr` representing the length of the string. + */ + charLength(): FunctionExpr { + return new FunctionExpr('char_length', [this]); + } + + /** + * Creates an expression that performs a case-sensitive string comparison. + * + * ```typescript + * // Check if the 'title' field contains the word "guide" (case-sensitive) + * field("title").like("%guide%"); + * ``` + * + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new `Expr` representing the 'like' comparison. + */ + like(pattern: string): FunctionExpr; + + /** + * Creates an expression that performs a case-sensitive string comparison. + * + * ```typescript + * // Check if the 'title' field contains the word "guide" (case-sensitive) + * field("title").like("%guide%"); + * ``` + * + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new `Expr` representing the 'like' comparison. + */ + like(pattern: Expr): FunctionExpr; + like(stringOrExpr: string | Expr): FunctionExpr { + return new FunctionExpr('like', [this, valueToDefaultExpr(stringOrExpr)]); + } + + /** + * Creates an expression that checks if a string contains a specified regular expression as a + * substring. + * + * ```typescript + * // Check if the 'description' field contains "example" (case-insensitive) + * field("description").regexContains("(?i)example"); + * ``` + * + * @param pattern The regular expression to use for the search. + * @return A new `Expr` representing the 'contains' comparison. + */ + regexContains(pattern: string): BooleanExpr; + + /** + * Creates an expression that checks if a string contains a specified regular expression as a + * substring. + * + * ```typescript + * // Check if the 'description' field contains the regular expression stored in field 'regex' + * field("description").regexContains(field("regex")); + * ``` + * + * @param pattern The regular expression to use for the search. + * @return A new `Expr` representing the 'contains' comparison. + */ + regexContains(pattern: Expr): BooleanExpr; + regexContains(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('regex_contains', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); + } + + /** + * Creates an expression that checks if a string matches a specified regular expression. + * + * ```typescript + * // Check if the 'email' field matches a valid email pattern + * field("email").regexMatch("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); + * ``` + * + * @param pattern The regular expression to use for the match. + * @return A new `Expr` representing the regular expression match. + */ + regexMatch(pattern: string): BooleanExpr; + + /** + * Creates an expression that checks if a string matches a specified regular expression. + * + * ```typescript + * // Check if the 'email' field matches a regular expression stored in field 'regex' + * field("email").regexMatch(field("regex")); + * ``` + * + * @param pattern The regular expression to use for the match. + * @return A new `Expr` representing the regular expression match. + */ + regexMatch(pattern: Expr): BooleanExpr; + regexMatch(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('regex_match', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); + } + + /** + * Creates an expression that checks if a string contains a specified substring. + * + * ```typescript + * // Check if the 'description' field contains "example". + * field("description").strContains("example"); + * ``` + * + * @param substring The substring to search for. + * @return A new `Expr` representing the 'contains' comparison. + */ + strContains(substring: string): BooleanExpr; + + /** + * Creates an expression that checks if a string contains the string represented by another expression. + * + * ```typescript + * // Check if the 'description' field contains the value of the 'keyword' field. + * field("description").strContains(field("keyword")); + * ``` + * + * @param expr The expression representing the substring to search for. + * @return A new `Expr` representing the 'contains' comparison. + */ + strContains(expr: Expr): BooleanExpr; + strContains(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('str_contains', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); + } + + /** + * Creates an expression that checks if a string starts with a given prefix. + * + * ```typescript + * // Check if the 'name' field starts with "Mr." + * field("name").startsWith("Mr."); + * ``` + * + * @param prefix The prefix to check for. + * @return A new `Expr` representing the 'starts with' comparison. + */ + startsWith(prefix: string): BooleanExpr; + + /** + * Creates an expression that checks if a string starts with a given prefix (represented as an + * expression). + * + * ```typescript + * // Check if the 'fullName' field starts with the value of the 'firstName' field + * field("fullName").startsWith(field("firstName")); + * ``` + * + * @param prefix The prefix expression to check for. + * @return A new `Expr` representing the 'starts with' comparison. + */ + startsWith(prefix: Expr): BooleanExpr; + startsWith(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('starts_with', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); + } + + /** + * Creates an expression that checks if a string ends with a given postfix. + * + * ```typescript + * // Check if the 'filename' field ends with ".txt" + * field("filename").endsWith(".txt"); + * ``` + * + * @param suffix The postfix to check for. + * @return A new `Expr` representing the 'ends with' comparison. + */ + endsWith(suffix: string): BooleanExpr; + + /** + * Creates an expression that checks if a string ends with a given postfix (represented as an + * expression). + * + * ```typescript + * // Check if the 'url' field ends with the value of the 'extension' field + * field("url").endsWith(field("extension")); + * ``` + * + * @param suffix The postfix expression to check for. + * @return A new `Expr` representing the 'ends with' comparison. + */ + endsWith(suffix: Expr): BooleanExpr; + endsWith(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('ends_with', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); + } + + /** + * Creates an expression that converts a string to lowercase. + * + * ```typescript + * // Convert the 'name' field to lowercase + * field("name").toLower(); + * ``` + * + * @return A new `Expr` representing the lowercase string. + */ + toLower(): FunctionExpr { + return new FunctionExpr('to_lower', [this]); + } + + /** + * Creates an expression that converts a string to uppercase. + * + * ```typescript + * // Convert the 'title' field to uppercase + * field("title").toUpper(); + * ``` + * + * @return A new `Expr` representing the uppercase string. + */ + toUpper(): FunctionExpr { + return new FunctionExpr('to_upper', [this]); + } + + /** + * Creates an expression that removes leading and trailing whitespace from a string. + * + * ```typescript + * // Trim whitespace from the 'userInput' field + * field("userInput").trim(); + * ``` + * + * @return A new `Expr` representing the trimmed string. + */ + trim(): FunctionExpr { + return new FunctionExpr('trim', [this]); + } + + /** + * Creates an expression that concatenates string expressions together. + * + * ```typescript + * // Combine the 'firstName', " ", and 'lastName' fields into a single string + * field("firstName").strConcat(constant(" "), field("lastName")); + * ``` + * + * @param secondString The additional expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or string literals to concatenate. + * @return A new `Expr` representing the concatenated string. + */ + strConcat( + secondString: Expr | string, + ...otherStrings: Array + ): FunctionExpr { + const elements = [secondString, ...otherStrings]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpr('str_concat', [this, ...exprs]); + } + + /** + * Creates an expression that reverses this string expression. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * field("myString").reverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed string. + */ + reverse(): FunctionExpr { + return new FunctionExpr('reverse', [this]); + } + + /** + * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring. + * + * ```typescript + * // Replace the first occurrence of "hello" with "hi" in the 'message' field + * field("message").replaceFirst("hello", "hi"); + * ``` + * + * @param find The substring to search for. + * @param replace The substring to replace the first occurrence of 'find' with. + * @return A new {@code Expr} representing the string with the first occurrence replaced. + */ + replaceFirst(find: string, replace: string): FunctionExpr; + + /** + * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring, + * where the substring to find and the replacement substring are specified by expressions. + * + * ```typescript + * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field + * field("message").replaceFirst(field("findField"), field("replaceField")); + * ``` + * + * @param find The expression representing the substring to search for. + * @param replace The expression representing the substring to replace the first occurrence of 'find' with. + * @return A new {@code Expr} representing the string with the first occurrence replaced. + */ + replaceFirst(find: Expr, replace: Expr): FunctionExpr; + replaceFirst(find: Expr | string, replace: Expr | string): FunctionExpr { + return new FunctionExpr('replace_first', [ + this, + valueToDefaultExpr(find), + valueToDefaultExpr(replace) + ]); + } + + /** + * Creates an expression that replaces all occurrences of a substring within this string expression with another substring. + * + * ```typescript + * // Replace all occurrences of "hello" with "hi" in the 'message' field + * field("message").replaceAll("hello", "hi"); + * ``` + * + * @param find The substring to search for. + * @param replace The substring to replace all occurrences of 'find' with. + * @return A new {@code Expr} representing the string with all occurrences replaced. + */ + replaceAll(find: string, replace: string): FunctionExpr; + + /** + * Creates an expression that replaces all occurrences of a substring within this string expression with another substring, + * where the substring to find and the replacement substring are specified by expressions. + * + * ```typescript + * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field + * field("message").replaceAll(field("findField"), field("replaceField")); + * ``` + * + * @param find The expression representing the substring to search for. + * @param replace The expression representing the substring to replace all occurrences of 'find' with. + * @return A new {@code Expr} representing the string with all occurrences replaced. + */ + replaceAll(find: Expr, replace: Expr): FunctionExpr; + replaceAll(find: Expr | string, replace: Expr | string): FunctionExpr { + return new FunctionExpr('replace_all', [ + this, + valueToDefaultExpr(find), + valueToDefaultExpr(replace) + ]); + } + + /** + * Creates an expression that calculates the length of this string expression in bytes. + * + * ```typescript + * // Calculate the length of the 'myString' field in bytes. + * field("myString").byteLength(); + * ``` + * + * @return A new {@code Expr} representing the length of the string in bytes. + */ + byteLength(): FunctionExpr { + return new FunctionExpr('byte_length', [this]); + } + + /** + * Accesses a value from a map (object) field using the provided key. + * + * ```typescript + * // Get the 'city' value from the 'address' map field + * field("address").mapGet("city"); + * ``` + * + * @param subfield The key to access in the map. + * @return A new `Expr` representing the value associated with the given key in the map. + */ + mapGet(subfield: string): FunctionExpr { + return new FunctionExpr('map_get', [this, constant(subfield)]); + } + + /** + * Creates an aggregation that counts the number of stage inputs with valid evaluations of the + * expression or field. + * + * ```typescript + * // Count the total number of products + * field("productId").count().as("totalProducts"); + * ``` + * + * @return A new `AggregateFunction` representing the 'count' aggregation. + */ + count(): AggregateFunction { + return new AggregateFunction('count', [this]); + } + + /** + * Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. + * + * ```typescript + * // Calculate the total revenue from a set of orders + * field("orderAmount").sum().as("totalRevenue"); + * ``` + * + * @return A new `AggregateFunction` representing the 'sum' aggregation. + */ + sum(): AggregateFunction { + return new AggregateFunction('sum', [this]); + } + + /** + * Creates an aggregation that calculates the average (mean) of a numeric field across multiple + * stage inputs. + * + * ```typescript + * // Calculate the average age of users + * field("age").avg().as("averageAge"); + * ``` + * + * @return A new `AggregateFunction` representing the 'avg' aggregation. + */ + avg(): AggregateFunction { + return new AggregateFunction('avg', [this]); + } + + /** + * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. + * + * ```typescript + * // Find the lowest price of all products + * field("price").minimum().as("lowestPrice"); + * ``` + * + * @return A new `AggregateFunction` representing the 'min' aggregation. + */ + minimum(): AggregateFunction { + return new AggregateFunction('minimum', [this]); + } + + /** + * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. + * + * ```typescript + * // Find the highest score in a leaderboard + * field("score").maximum().as("highestScore"); + * ``` + * + * @return A new `AggregateFunction` representing the 'max' aggregation. + */ + maximum(): AggregateFunction { + return new AggregateFunction('maximum', [this]); + } + + /** + * Creates an expression that returns the larger value between this expression and another expression, based on Firestore's value type ordering. + * + * ```typescript + * // Returns the larger value between the 'timestamp' field and the current timestamp. + * field("timestamp").logicalMaximum(Function.currentTimestamp()); + * ``` + * + * @param second The second expression or literal to compare with. + * @param others Optional additional expressions or literals to compare with. + * @return A new {@code Expr} representing the logical max operation. + */ + logicalMaximum( + second: Expr | unknown, + ...others: Array + ): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('logical_maximum', [ + this, + ...values.map(valueToDefaultExpr) + ]); + } + + /** + * Creates an expression that returns the smaller value between this expression and another expression, based on Firestore's value type ordering. + * + * ```typescript + * // Returns the smaller value between the 'timestamp' field and the current timestamp. + * field("timestamp").logicalMinimum(Function.currentTimestamp()); + * ``` + * + * @param second The second expression or literal to compare with. + * @param others Optional additional expressions or literals to compare with. + * @return A new {@code Expr} representing the logical min operation. + */ + logicalMinimum( + second: Expr | unknown, + ...others: Array + ): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('logical_minimum', [ + this, + ...values.map(valueToDefaultExpr) + ]); + } + + /** + * Creates an expression that calculates the length (number of dimensions) of this Firestore Vector expression. + * + * ```typescript + * // Get the vector length (dimension) of the field 'embedding'. + * field("embedding").vectorLength(); + * ``` + * + * @return A new {@code Expr} representing the length of the vector. + */ + vectorLength(): FunctionExpr { + return new FunctionExpr('vector_length', [this]); + } + + /** + * Calculates the cosine distance between two vectors. + * + * ```typescript + * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field + * field("userVector").cosineDistance(field("itemVector")); + * ``` + * + * @param vectorExpression The other vector (represented as an Expr) to compare against. + * @return A new `Expr` representing the cosine distance between the two vectors. + */ + cosineDistance(vectorExpression: Expr): FunctionExpr; + /** + * Calculates the Cosine distance between two vectors. + * + * ```typescript + * // Calculate the Cosine distance between the 'location' field and a target location + * field("location").cosineDistance(new VectorValue([37.7749, -122.4194])); + * ``` + * + * @param vector The other vector (as a VectorValue) to compare against. + * @return A new `Expr` representing the Cosine* distance between the two vectors. + */ + cosineDistance(vector: VectorValue | number[]): FunctionExpr; + cosineDistance(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)]); + } + + /** + * Calculates the dot product between two vectors. + * + * ```typescript + * // Calculate the dot product between a feature vector and a target vector + * field("features").dotProduct([0.5, 0.8, 0.2]); + * ``` + * + * @param vectorExpression The other vector (as an array of numbers) to calculate with. + * @return A new `Expr` representing the dot product between the two vectors. + */ + dotProduct(vectorExpression: Expr): FunctionExpr; + + /** + * Calculates the dot product between two vectors. + * + * ```typescript + * // Calculate the dot product between a feature vector and a target vector + * field("features").dotProduct(new VectorValue([0.5, 0.8, 0.2])); + * ``` + * + * @param vector The other vector (as an array of numbers) to calculate with. + * @return A new `Expr` representing the dot product between the two vectors. + */ + dotProduct(vector: VectorValue | number[]): FunctionExpr; + dotProduct(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('dot_product', [this, vectorToExpr(other)]); + } + + /** + * Calculates the Euclidean distance between two vectors. + * + * ```typescript + * // Calculate the Euclidean distance between the 'location' field and a target location + * field("location").euclideanDistance([37.7749, -122.4194]); + * ``` + * + * @param vectorExpression The other vector (as an array of numbers) to calculate with. + * @return A new `Expr` representing the Euclidean distance between the two vectors. + */ + euclideanDistance(vectorExpression: Expr): FunctionExpr; + + /** + * Calculates the Euclidean distance between two vectors. + * + * ```typescript + * // Calculate the Euclidean distance between the 'location' field and a target location + * field("location").euclideanDistance(new VectorValue([37.7749, -122.4194])); + * ``` + * + * @param vector The other vector (as a VectorValue) to compare against. + * @return A new `Expr` representing the Euclidean distance between the two vectors. + */ + euclideanDistance(vector: VectorValue | number[]): FunctionExpr; + euclideanDistance(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)]); + } + + /** + * Creates an expression that interprets this expression as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'microseconds' field as microseconds since epoch. + * field("microseconds").unixMicrosToTimestamp(); + * ``` + * + * @return A new {@code Expr} representing the timestamp. + */ + unixMicrosToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_micros_to_timestamp', [this]); + } + + /** + * Creates an expression that converts this timestamp expression to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to microseconds since epoch. + * field("timestamp").timestampToUnixMicros(); + * ``` + * + * @return A new {@code Expr} representing the number of microseconds since epoch. + */ + timestampToUnixMicros(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_micros', [this]); + } + + /** + * Creates an expression that interprets this expression as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'milliseconds' field as milliseconds since epoch. + * field("milliseconds").unixMillisToTimestamp(); + * ``` + * + * @return A new {@code Expr} representing the timestamp. + */ + unixMillisToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_millis_to_timestamp', [this]); + } + + /** + * Creates an expression that converts this timestamp expression to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to milliseconds since epoch. + * field("timestamp").timestampToUnixMillis(); + * ``` + * + * @return A new {@code Expr} representing the number of milliseconds since epoch. + */ + timestampToUnixMillis(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_millis', [this]); + } + + /** + * Creates an expression that interprets this expression as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'seconds' field as seconds since epoch. + * field("seconds").unixSecondsToTimestamp(); + * ``` + * + * @return A new {@code Expr} representing the timestamp. + */ + unixSecondsToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_seconds_to_timestamp', [this]); + } + + /** + * Creates an expression that converts this timestamp expression to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to seconds since epoch. + * field("timestamp").timestampToUnixSeconds(); + * ``` + * + * @return A new {@code Expr} representing the number of seconds since epoch. + */ + timestampToUnixSeconds(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_seconds', [this]); + } + + /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * ```typescript + * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. + * field("timestamp").timestampAdd(field("unit"), field("amount")); + * ``` + * + * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. + * @param amount The expression evaluates to amount of the unit. + * @return A new {@code Expr} representing the resulting timestamp. + */ + timestampAdd(unit: Expr, amount: Expr): FunctionExpr; + + /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * ```typescript + * // Add 1 day to the 'timestamp' field. + * field("timestamp").timestampAdd("day", 1); + * ``` + * + * @param unit The unit of time to add (e.g., "day", "hour"). + * @param amount The amount of time to add. + * @return A new {@code Expr} representing the resulting timestamp. + */ + timestampAdd( + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number + ): FunctionExpr; + timestampAdd( + unit: + | Expr + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expr | number + ): FunctionExpr { + return new FunctionExpr('timestamp_add', [ + this, + valueToDefaultExpr(unit), + valueToDefaultExpr(amount) + ]); + } + + /** + * Creates an expression that subtracts a specified amount of time from this timestamp expression. + * + * ```typescript + * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. + * field("timestamp").timestampSub(field("unit"), field("amount")); + * ``` + * + * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. + * @param amount The expression evaluates to amount of the unit. + * @return A new {@code Expr} representing the resulting timestamp. + */ + timestampSub(unit: Expr, amount: Expr): FunctionExpr; + + /** + * Creates an expression that subtracts a specified amount of time from this timestamp expression. + * + * ```typescript + * // Subtract 1 day from the 'timestamp' field. + * field("timestamp").timestampSub("day", 1); + * ``` + * + * @param unit The unit of time to subtract (e.g., "day", "hour"). + * @param amount The amount of time to subtract. + * @return A new {@code Expr} representing the resulting timestamp. + */ + timestampSub( + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number + ): FunctionExpr; + timestampSub( + unit: + | Expr + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expr | number + ): FunctionExpr { + return new FunctionExpr('timestamp_sub', [ + this, + valueToDefaultExpr(unit), + valueToDefaultExpr(amount) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise AND operation between this expression and a constant. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 0xFF. + * field("field1").bitAnd(0xFF); + * ``` + * + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. + */ + bitAnd(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise AND operation between two expressions. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * field("field1").bitAnd(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. + */ + bitAnd(bitsExpression: Expr): FunctionExpr; + bitAnd(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_and', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise OR operation between this expression and a constant. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 0xFF. + * field("field1").bitOr(0xFF); + * ``` + * + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. + */ + bitOr(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise OR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * field("field1").bitOr(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. + */ + bitOr(bitsExpression: Expr): FunctionExpr; + bitOr(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_or', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between this expression and a constant. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * field("field1").bitXor(0xFF); + * ``` + * + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ + bitXor(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * field("field1").bitXor(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ + bitXor(bitsExpression: Expr): FunctionExpr; + bitXor(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_xor', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise NOT operation to this expression. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * field("field1").bitNot(); + * ``` + * + * @return A new {@code Expr} representing the bitwise NOT operation. + */ + bitNot(): FunctionExpr { + return new FunctionExpr('bit_not', [this]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise left shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * field("field1").bitLeftShift(2); + * ``` + * + * @param y The operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ + bitLeftShift(y: number): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise left shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * field("field1").bitLeftShift(field("field2")); + * ``` + * + * @param numberExpr The operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ + bitLeftShift(numberExpr: Expr): FunctionExpr; + bitLeftShift(numberExpr: number | Expr): FunctionExpr { + return new FunctionExpr('bit_left_shift', [ + this, + valueToDefaultExpr(numberExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise right shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * field("field1").bitRightShift(2); + * ``` + * + * @param right The operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ + bitRightShift(y: number): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise right shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * field("field1").bitRightShift(field("field2")); + * ``` + * + * @param numberExpr The operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ + bitRightShift(numberExpr: Expr): FunctionExpr; + bitRightShift(numberExpr: number | Expr): FunctionExpr { + return new FunctionExpr('bit_right_shift', [ + this, + valueToDefaultExpr(numberExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that returns the document ID from a path. + * + * ```typescript + * // Get the document ID from a path. + * field("__path__").documentId(); + * ``` + * + * @return A new {@code Expr} representing the documentId operation. + */ + documentId(): FunctionExpr { + return new FunctionExpr('document_id', [this]); + } + + /** + * @beta + * + * Creates an expression that returns a substring of the results of this expression. + * + * @param position Index of the first character of the substring. + * @param length Length of the substring. If not provided, the substring will + * end at the end of the input. + */ + substr(position: number, length?: number): FunctionExpr; + + /** + * @beta + * + * Creates an expression that returns a substring of the results of this expression. + * + * @param position An expression returning the index of the first character of the substring. + * @param length An expression returning the length of the substring. If not provided the + * substring will end at the end of the input. + */ + substr(position: Expr, length?: Expr): FunctionExpr; + substr(position: Expr | number, length?: Expr | number): FunctionExpr { + const positionExpr = valueToDefaultExpr(position); + if (length === undefined) { + return new FunctionExpr('substr', [this, positionExpr]); + } else { + return new FunctionExpr('substr', [ + this, + positionExpr, + valueToDefaultExpr(length) + ]); + } + } + + /** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and returns the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the 'tags' field array at index `1`. + * field('tags').arrayOffset(1); + * ``` + * + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ + arrayOffset(offset: number): FunctionExpr; + + /** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and returns the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * field('tags').arrayOffset(field('favoriteTag')); + * ``` + * + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ + arrayOffset(offsetExpr: Expr): FunctionExpr; + arrayOffset(offset: Expr | number): FunctionExpr { + return new FunctionExpr('array_offset', [this, valueToDefaultExpr(offset)]); + } + + /** + * @beta + * + * Creates an expression that checks if a given expression produces an error. + * + * ```typescript + * // Check if the result of a calculation is an error + * field("title").arrayContains(1).isError(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isError' check. + */ + isError(): BooleanExpr { + return new BooleanExpr('is_error', [this]); + } + + /** + * @beta + * + * Creates an expression that returns the result of the `catchExpr` argument + * if there is an error, else return the result of this expression. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // the entire title field if the array is empty or the field is another type. + * field("title").arrayOffset(0).ifError(field("title")); + * ``` + * + * @param catchExpr The catch expression that will be evaluated and + * returned if this expression produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchExpr: Expr): FunctionExpr; + + /** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of this expression. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // "Default Title" + * field("title").arrayOffset(0).ifError("Default Title"); + * ``` + * + * @param catchValue The value that will be returned if this expression + * produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchValue: unknown): FunctionExpr; + ifError(catchValue: unknown): FunctionExpr { + return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)]); + } + + /** + * @beta + * + * Creates an expression that returns `true` if the result of this expression + * is absent. Otherwise, returns `false` even if the value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * field("value").isAbsent(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isAbsent' check. + */ + isAbsent(): BooleanExpr { + return new BooleanExpr('is_absent', [this]); + } + + /** + * @beta + * + * Creates an expression that checks if tbe result of an expression is not null. + * + * ```typescript + * // Check if the value of the 'name' field is not null + * field("name").isNotNull(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isNotNull' check. + */ + isNotNull(): BooleanExpr { + return new BooleanExpr('is_not_null', [this]); + } + + /** + * @beta + * + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NOT NaN + * field("value").divide(0).isNotNan(); + * ``` + * + * @return A new {@code Expr} representing the 'isNaN' check. + */ + isNotNan(): BooleanExpr { + return new BooleanExpr('is_not_nan', [this]); + } + + /** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating this expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * map({foo: 'bar', baz: true}).mapRemove('baz'); + * ``` + * + * @param key The name of the key to remove from the input map. + * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. + */ + mapRemove(key: string): FunctionExpr; + /** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating this expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * map({foo: 'bar', baz: true}).mapRemove(constant('baz')); + * ``` + * + * @param keyExpr An expression that produces the name of the key to remove from the input map. + * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. + */ + mapRemove(keyExpr: Expr): FunctionExpr; + mapRemove(stringExpr: Expr | string): FunctionExpr { + return new FunctionExpr('map_remove', [ + this, + valueToDefaultExpr(stringExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * field('settings').mapMerge({ enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param secondMap A required second map to merge. Represented as a literal or + * an expression that returns a map. + * @param otherMaps Optional additional maps to merge. Each map is represented + * as a literal or an expression that returns a map. + * + * @returns A new {@code FirestoreFunction} representing the 'mapMerge' operation. + */ + mapMerge( + secondMap: Record | Expr, + ...otherMaps: Array | Expr> + ): FunctionExpr { + const secondMapExpr = valueToDefaultExpr(secondMap); + const otherMapExprs = otherMaps.map(valueToDefaultExpr); + return new FunctionExpr('map_merge', [ + this, + secondMapExpr, + ...otherMapExprs + ]); + } + + /** + * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * + * ```typescript + * // Sort documents by the 'name' field in ascending order + * pipeline().collection("users") + * .sort(field("name").ascending()); + * ``` + * + * @return A new `Ordering` for ascending sorting. + */ + ascending(): Ordering { + return ascending(this); + } + + /** + * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * + * ```typescript + * // Sort documents by the 'createdAt' field in descending order + * firestore.pipeline().collection("users") + * .sort(field("createdAt").descending()); + * ``` + * + * @return A new `Ordering` for descending sorting. + */ + descending(): Ordering { + return descending(this); + } + + /** + * Assigns an alias to this expression. + * + * Aliases are useful for renaming fields in the output of a stage or for giving meaningful + * names to calculated values. + * + * ```typescript + * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. + * firestore.pipeline().collection("items") + * .addFields(field("price").multiply(field("quantity")).as("totalPrice")); + * ``` + * + * @param name The alias to assign to this expression. + * @return A new {@link ExprWithAlias} that wraps this + * expression and associates it with the provided alias. + */ + as(name: string): ExprWithAlias { + return new ExprWithAlias(this, name); + } +} + +/** + * @beta + * + * An interface that represents a selectable expression. + */ +export interface Selectable { + selectable: true; + readonly alias: string; + readonly expr: Expr; +} + +/** + * @beta + * + * A class that represents an aggregate function. + */ +export class AggregateFunction implements ProtoValueSerializable, UserData { + exprType: ExprType = 'AggregateFunction'; + + /** + * @internal + * @private + * Indicates if this expression was created from a literal value passed + * by the caller. + */ + _createdFromLiteral: boolean = false; + + constructor(private name: string, private params: Expr[]) {} + + /** + * Assigns an alias to this AggregateFunction. The alias specifies the name that + * the aggregated value will have in the output document. + * + * ```typescript + * // Calculate the average price of all items and assign it the alias "averagePrice". + * firestore.pipeline().collection("items") + * .aggregate(field("price").avg().as("averagePrice")); + * ``` + * + * @param name The alias to assign to this AggregateFunction. + * @return A new {@link AggregateWithAlias} that wraps this + * AggregateFunction and associates it with the provided alias. + */ + as(name: string): AggregateWithAlias { + return new AggregateWithAlias(this, name); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + functionValue: { + name: this.name, + args: this.params.map(p => p._toProto(serializer)) + } + }; + } + + _protoValueType = 'ProtoValue' as const; + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, this.name); + this.params.forEach(expr => { + return expr._readUserData(dataReader, context); + }); + } +} + +/** + * @beta + * + * An AggregateFunction with alias. + */ +export class AggregateWithAlias implements UserData { + constructor(readonly aggregate: AggregateFunction, readonly alias: string) {} + + /** + * @internal + * @private + * Indicates if this expression was created from a literal value passed + * by the caller. + */ + _createdFromLiteral: boolean = false; + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, 'as'); + this.aggregate._readUserData(dataReader, context); + } +} + +/** + * @beta + */ +export class ExprWithAlias implements Selectable, UserData { + exprType: ExprType = 'ExprWithAlias'; + selectable = true as const; + + /** + * @internal + * @private + * Indicates if this expression was created from a literal value passed + * by the caller. + */ + _createdFromLiteral: boolean = false; + + constructor(readonly expr: Expr, readonly alias: string) {} + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, 'as'); + this.expr._readUserData(dataReader, context); + } +} + +/** + * @internal + */ +class ListOfExprs extends Expr implements UserData { + exprType: ExprType = 'ListOfExprs'; + + constructor(private exprs: Expr[]) { + super(); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + arrayValue: { + values: this.exprs.map(p => p._toProto(serializer)!) + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader): void { + this.exprs.forEach((expr: Expr) => expr._readUserData(dataReader)); + } +} + +/** + * @beta + * + * Represents a reference to a field in a Firestore document, or outputs of a {@link Pipeline} stage. + * + *

Field references are used to access document field values in expressions and to specify fields + * for sorting, filtering, and projecting data in Firestore pipelines. + * + *

You can create a `Field` instance using the static {@link #of} method: + * + * ```typescript + * // Create a Field instance for the 'name' field + * const nameField = field("name"); + * + * // Create a Field instance for a nested field 'address.city' + * const cityField = field("address.city"); + * ``` + */ +export class Field extends Expr implements Selectable { + readonly exprType: ExprType = 'Field'; + selectable = true as const; + + /** + * @internal + * @private + * @hideconstructor + * @param fieldPath + */ + constructor(private fieldPath: InternalFieldPath) { + super(); + } + + fieldName(): string { + return this.fieldPath.canonicalString(); + } + + get alias(): string { + return this.fieldName(); + } + + get expr(): Expr { + return this; + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + fieldReferenceValue: this.fieldPath.canonicalString() + }; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader): void {} +} + +/** + * Creates a {@code Field} instance representing the field at the given path. + * + * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * ```typescript + * // Create a Field instance for the 'title' field + * const titleField = field("title"); + * + * // Create a Field instance for a nested field 'author.firstName' + * const authorFirstNameField = field("author.firstName"); + * ``` + * + * @param name The path to the field. + * @return A new {@code Field} instance representing the specified field. + */ +export function field(name: string): Field; +export function field(path: FieldPath): Field; +export function field(nameOrPath: string | FieldPath): Field { + if (typeof nameOrPath === 'string') { + if (DOCUMENT_KEY_NAME === nameOrPath) { + return new Field(documentIdFieldPath()._internalPath); + } + return new Field(fieldPathFromArgument('field', nameOrPath)); + } else { + return new Field(nameOrPath._internalPath); + } +} + +/** + * @beta + * + * Represents a constant value that can be used in a Firestore pipeline expression. + * + * You can create a `Constant` instance using the static {@link #of} method: + * + * ```typescript + * // Create a Constant instance for the number 10 + * const ten = constant(10); + * + * // Create a Constant instance for the string "hello" + * const hello = constant("hello"); + * ``` + */ +export class Constant extends Expr { + readonly exprType: ExprType = 'Constant'; + + private _protoValue?: ProtoValue; + + /** + * @private + * @internal + * @hideconstructor + * @param value The value of the constant. + */ + constructor(private value: unknown) { + super(); + } + + /** + * @private + * @internal + */ + static _fromProto(value: ProtoValue): Constant { + const result = new Constant(value); + result._protoValue = value; + return result; + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + hardAssert( + this._protoValue !== undefined, + 0x00ed, + 'Value of this constant has not been serialized to proto value' + ); + return this._protoValue; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, 'constant'); + + if (isFirestoreValue(this._protoValue)) { + return; + } else { + this._protoValue = parseData(this.value, context)!; + } + } +} + +/** + * Creates a `Constant` instance for a number value. + * + * @param value The number value. + * @return A new `Constant` instance. + */ +export function constant(value: number): Constant; + +/** + * Creates a `Constant` instance for a string value. + * + * @param value The string value. + * @return A new `Constant` instance. + */ +export function constant(value: string): Constant; + +/** + * Creates a `Constant` instance for a boolean value. + * + * @param value The boolean value. + * @return A new `Constant` instance. + */ +export function constant(value: boolean): Constant; + +/** + * Creates a `Constant` instance for a null value. + * + * @param value The null value. + * @return A new `Constant` instance. + */ +export function constant(value: null): Constant; + +/** + * Creates a `Constant` instance for a GeoPoint value. + * + * @param value The GeoPoint value. + * @return A new `Constant` instance. + */ +export function constant(value: GeoPoint): Constant; + +/** + * Creates a `Constant` instance for a Timestamp value. + * + * @param value The Timestamp value. + * @return A new `Constant` instance. + */ +export function constant(value: Timestamp): Constant; + +/** + * Creates a `Constant` instance for a Date value. + * + * @param value The Date value. + * @return A new `Constant` instance. + */ +export function constant(value: Date): Constant; + +/** + * Creates a `Constant` instance for a Bytes value. + * + * @param value The Bytes value. + * @return A new `Constant` instance. + */ +export function constant(value: Bytes): Constant; + +/** + * Creates a `Constant` instance for a DocumentReference value. + * + * @param value The DocumentReference value. + * @return A new `Constant` instance. + */ +export function constant(value: DocumentReference): Constant; + +/** + * Creates a `Constant` instance for a Firestore proto value. + * For internal use only. + * @private + * @internal + * @param value The Firestore proto value. + * @return A new `Constant` instance. + */ +export function constant(value: ProtoValue): Constant; + +/** + * Creates a `Constant` instance for a VectorValue value. + * + * @param value The VectorValue value. + * @return A new `Constant` instance. + */ +export function constant(value: VectorValue): Constant; + +export function constant(value: unknown): Constant { + return new Constant(value); +} + +/** + * Creates a `Constant` instance for a VectorValue value. + * + * ```typescript + * // Create a Constant instance for a vector value + * const vectorConstant = constantVector([1, 2, 3]); + * ``` + * + * @param value The VectorValue value. + * @return A new `Constant` instance. + */ +export function constantVector(value: number[] | VectorValue): Constant { + if (value instanceof VectorValue) { + return new Constant(value); + } else { + return new Constant(new VectorValue(value as number[])); + } +} + +/** + * Internal only + * @internal + * @private + */ +export class MapValue extends Expr { + constructor(private plainObject: Map) { + super(); + } + + exprType: ExprType = 'Constant'; + + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, '_map'); + + this.plainObject.forEach(expr => { + expr._readUserData(dataReader, context); + }); + } + + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return toMapValue(serializer, this.plainObject); + } +} + +/** + * @beta + * + * This class defines the base class for Firestore {@link Pipeline} functions, which can be evaluated within pipeline + * execution. + * + * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link eq}, + * or the methods on {@link Expr} ({@link Expr#eq}, {@link Expr#lt}, etc.) to construct new Function instances. + */ +export class FunctionExpr extends Expr { + readonly exprType: ExprType = 'Function'; + + constructor(private name: string, private params: Expr[]) { + super(); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + functionValue: { + name: this.name, + args: this.params.map(p => p._toProto(serializer)) + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, this.name); + this.params.forEach(expr => { + return expr._readUserData(dataReader, context); + }); + } +} + +/** + * @beta + * + * An interface that represents a filter condition. + */ +export class BooleanExpr extends FunctionExpr { + filterable: true = true; + + /** + * Creates an aggregation that finds the count of input documents satisfying + * this boolean expression. + * + * ```typescript + * // Find the count of documents with a score greater than 90 + * field("score").gt(90).countIf().as("highestScore"); + * ``` + * + * @return A new `AggregateFunction` representing the 'countIf' aggregation. + */ + countIf(): AggregateFunction { + return new AggregateFunction('count_if', [this]); + } + + /** + * Creates an expression that negates this boolean expression. + * + * ```typescript + * // Find documents where the 'tags' field does not contain 'completed' + * field("tags").arrayContains("completed").not(); + * ``` + * + * @return A new {@code Expr} representing the negated filter condition. + */ + not(): BooleanExpr { + return new BooleanExpr('not', [this]); + } +} + +/** + * @beta + * Creates an aggregation that counts the number of stage inputs where the provided + * boolean expression evaluates to true. + * + * ```typescript + * // Count the number of documents where 'is_active' field equals true + * countIf(field("is_active").eq(true)).as("numActiveDocuments"); + * ``` + * + * @param booleanExpr - The boolean expression to evaluate on each input. + * @returns A new `AggregateFunction` representing the 'countIf' aggregation. + */ +export function countIf(booleanExpr: BooleanExpr): AggregateFunction { + return booleanExpr.countIf(); +} + +/** + * @beta + * Creates an expression that return a pseudo-random value of type double in the + * range of [0, 1), inclusive of 0 and exclusive of 1. + * + * @returns A new `Expr` representing the 'rand' function. + */ +export function rand(): FunctionExpr { + return new FunctionExpr('rand', []); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise AND operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 0xFF. + * bitAnd("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. + */ +export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise AND operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * bitAnd("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. + */ +export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise AND operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 0xFF. + * bitAnd(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. + */ +export function bitAnd( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise AND operation between two expressions. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * bitAnd(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. + */ +export function bitAnd( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitAnd( + bits: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOrExpression(bits).bitAnd(valueToDefaultExpr(bitsOrExpression)); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise OR operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 0xFF. + * bitOr("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. + */ +export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise OR operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * bitOr("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. + */ +export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise OR operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 0xFF. + * bitOr(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. + */ +export function bitOr( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise OR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * bitOr(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. + */ +export function bitOr( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitOr( + bits: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOrExpression(bits).bitOr(valueToDefaultExpr(bitsOrExpression)); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * bitXor("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ +export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * bitXor("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ +export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * bitXor(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ +export function bitXor( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * bitXor(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ +export function bitXor( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitXor( + bits: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOrExpression(bits).bitXor(valueToDefaultExpr(bitsOrExpression)); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise NOT operation to a field. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * bitNot("field1"); + * ``` + * + * @param field The operand field name. + * @return A new {@code Expr} representing the bitwise NOT operation. + */ +export function bitNot(field: string): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise NOT operation to an expression. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * bitNot(field("field1")); + * ``` + * + * @param bitsValueExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise NOT operation. + */ +export function bitNot(bitsValueExpression: Expr): FunctionExpr; +export function bitNot(bits: string | Expr): FunctionExpr { + return fieldOrExpression(bits).bitNot(); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise left shift operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * bitLeftShift("field1", 2); + * ``` + * + * @param field The left operand field name. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ +export function bitLeftShift(field: string, y: number): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise left shift operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * bitLeftShift("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param numberExpr The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ +export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise left shift operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * bitLeftShift(field("field1"), 2); + * ``` + * + * @param xValue An expression returning bits. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ +export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise left shift operation between two expressions. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * bitLeftShift(field("field1"), field("field2")); + * ``` + * + * @param xValue An expression returning bits. + * @param numberExpr The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. + */ +export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; +export function bitLeftShift( + xValue: string | Expr, + numberExpr: number | Expr +): FunctionExpr { + return fieldOrExpression(xValue).bitLeftShift(valueToDefaultExpr(numberExpr)); +} + +/** + * @beta + * + * Creates an expression that applies a bitwise right shift operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * bitRightShift("field1", 2); + * ``` + * + * @param field The left operand field name. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ +export function bitRightShift(field: string, y: number): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise right shift operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * bitRightShift("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param numberExpr The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ +export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise right shift operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * bitRightShift(field("field1"), 2); + * ``` + * + * @param xValue An expression returning bits. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ +export function bitRightShift(xValue: Expr, y: number): FunctionExpr; +/** + * @beta + * + * Creates an expression that applies a bitwise right shift operation between two expressions. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * bitRightShift(field("field1"), field("field2")); + * ``` + * + * @param xValue An expression returning bits. + * @param right The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ +export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; +export function bitRightShift( + xValue: string | Expr, + numberExpr: number | Expr +): FunctionExpr { + return fieldOrExpression(xValue).bitRightShift( + valueToDefaultExpr(numberExpr) + ); +} + +/** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index 1. + * arrayOffset('tags', 1); + * ``` + * + * @param arrayField The name of the array field. + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ +export function arrayOffset(arrayField: string, offset: number): FunctionExpr; + +/** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * arrayOffset('tags', field('favoriteTag')); + * ``` + * + * @param arrayField The name of the array field. + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ +export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; + +/** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index 1. + * arrayOffset(field('tags'), 1); + * ``` + * + * @param arrayExpression An Expr evaluating to an array. + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ +export function arrayOffset( + arrayExpression: Expr, + offset: number +): FunctionExpr; + +/** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * arrayOffset(field('tags'), field('favoriteTag')); + * ``` + * + * @param arrayExpression An Expr evaluating to an array. + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ +export function arrayOffset( + arrayExpression: Expr, + offsetExpr: Expr +): FunctionExpr; +export function arrayOffset( + array: Expr | string, + offset: Expr | number +): FunctionExpr { + return fieldOrExpression(array).arrayOffset(valueToDefaultExpr(offset)); +} + +/** + * @beta + * + * Creates an expression that checks if a given expression produces an error. + * + * ```typescript + * // Check if the result of a calculation is an error + * isError(field("title").arrayContains(1)); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isError' check. + */ +export function isError(value: Expr): BooleanExpr { + return value.isError(); +} + +/** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // the entire title field if the array is empty or the field is another type. + * ifError(field("title").arrayOffset(0), field("title")); + * ``` + * + * @param tryExpr The try expression. + * @param catchExpr The catch expression that will be evaluated and + * returned if the tryExpr produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ +export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // "Default Title" + * ifError(field("title").arrayOffset(0), "Default Title"); + * ``` + * + * @param tryExpr The try expression. + * @param catchValue The value that will be returned if the tryExpr produces an + * error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ +export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr; +export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr { + return tryExpr.ifError(valueToDefaultExpr(catchValue)); +} + +/** + * @beta + * + * Creates an expression that returns `true` if a value is absent. Otherwise, + * returns `false` even if the value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * isAbsent(field("value")); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isAbsent' check. + */ +export function isAbsent(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that returns `true` if a field is absent. Otherwise, + * returns `false` even if the field value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * isAbsent("value"); + * ``` + * + * @param field The field to check. + * @return A new {@code Expr} representing the 'isAbsent' check. + */ +export function isAbsent(field: string): BooleanExpr; +export function isAbsent(value: Expr | string): BooleanExpr { + return fieldOrExpression(value).isAbsent(); +} + +/** + * @beta + * + * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * isNaN(field("value").divide(0)); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNull(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * isNaN("value"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNull(value: string): BooleanExpr; +export function isNull(value: Expr | string): BooleanExpr { + return fieldOrExpression(value).isNull(); +} + +/** + * @beta + * + * Creates an expression that checks if tbe result of an expression is not null. + * + * ```typescript + * // Check if the value of the 'name' field is not null + * isNotNull(field("name")); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNotNull(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if tbe value of a field is not null. + * + * ```typescript + * // Check if the value of the 'name' field is not null + * isNotNull("name"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNotNull(value: string): BooleanExpr; +export function isNotNull(value: Expr | string): BooleanExpr { + return fieldOrExpression(value).isNotNull(); +} + +/** + * @beta + * + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NOT NaN + * isNotNaN(field("value").divide(0)); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isNotNaN' check. + */ +export function isNotNan(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). + * + * ```typescript + * // Check if the value of a field is NOT NaN + * isNotNaN("value"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNotNaN' check. + */ +export function isNotNan(value: string): BooleanExpr; +export function isNotNan(value: Expr | string): BooleanExpr { + return fieldOrExpression(value).isNotNan(); +} + +/** + * @beta + * + * Creates an expression that removes a key from the map at the specified field name. + * + * ``` + * // Removes the key 'city' field from the map in the address field of the input document. + * mapRemove('address', 'city'); + * ``` + * + * @param mapField The name of a field containing a map value. + * @param key The name of the key to remove from the input map. + */ +export function mapRemove(mapField: string, key: string): FunctionExpr; +/** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * mapRemove(map({foo: 'bar', baz: true}), 'baz'); + * ``` + * + * @param mapExpr An expression return a map value. + * @param key The name of the key to remove from the input map. + */ +export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; +/** + * @beta + * + * Creates an expression that removes a key from the map at the specified field name. + * + * ``` + * // Removes the key 'city' field from the map in the address field of the input document. + * mapRemove('address', constant('city')); + * ``` + * + * @param mapField The name of a field containing a map value. + * @param keyExpr An expression that produces the name of the key to remove from the input map. + */ +export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; +/** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * mapRemove(map({foo: 'bar', baz: true}), constant('baz')); + * ``` + * + * @param mapExpr An expression return a map value. + * @param keyExpr An expression that produces the name of the key to remove from the input map. + */ +export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; + +export function mapRemove( + mapExpr: Expr | string, + stringExpr: Expr | string +): FunctionExpr { + return fieldOrExpression(mapExpr).mapRemove(valueToDefaultExpr(stringExpr)); +} + +/** + * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * mapMerge('settings', { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param mapField Name of a field containing a map value that will be merged. + * @param secondMap A required second map to merge. Represented as a literal or + * an expression that returns a map. + * @param otherMaps Optional additional maps to merge. Each map is represented + * as a literal or an expression that returns a map. + */ +export function mapMerge( + mapField: string, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * mapMerge(field('settings'), { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param firstMap An expression or literal map value that will be merged. + * @param secondMap A required second map to merge. Represented as a literal or + * an expression that returns a map. + * @param otherMaps Optional additional maps to merge. Each map is represented + * as a literal or an expression that returns a map. + */ +export function mapMerge( + firstMap: Record | Expr, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr; + +export function mapMerge( + firstMap: string | Record | Expr, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr { + const secondMapExpr = valueToDefaultExpr(secondMap); + const otherMapExprs = otherMaps.map(valueToDefaultExpr); + return fieldOrExpression(firstMap).mapMerge(secondMapExpr, ...otherMapExprs); +} + +/** + * @beta + * + * Creates an expression that returns the document ID from a path. + * + * ```typescript + * // Get the document ID from a path. + * documentId(myDocumentReference); + * ``` + * + * @return A new {@code Expr} representing the documentId operation. + */ +export function documentId( + documentPath: string | DocumentReference +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns the document ID from a path. + * + * ```typescript + * // Get the document ID from a path. + * documentId(field("__path__")); + * ``` + * + * @return A new {@code Expr} representing the documentId operation. + */ +export function documentId(documentPathExpr: Expr): FunctionExpr; + +export function documentId( + documentPath: Expr | string | DocumentReference +): FunctionExpr { + // @ts-ignore + const documentPathExpr = valueToDefaultExpr(documentPath); + return documentPathExpr.documentId(); +} + +/** + * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param field The name of a field containing a string or byte array to compute the substring from. + * @param position Index of the first character of the substring. + * @param length Length of the substring. + */ +export function substr( + field: string, + position: number, + length?: number +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param input An expression returning a string or byte array to compute the substring from. + * @param position Index of the first character of the substring. + * @param length Length of the substring. + */ +export function substr( + input: Expr, + position: number, + length?: number +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param field The name of a field containing a string or byte array to compute the substring from. + * @param position An expression that returns the index of the first character of the substring. + * @param length An expression that returns the length of the substring. + */ +export function substr( + field: string, + position: Expr, + length?: Expr +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param input An expression returning a string or byte array to compute the substring from. + * @param position An expression that returns the index of the first character of the substring. + * @param length An expression that returns the length of the substring. + */ +export function substr( + input: Expr, + position: Expr, + length?: Expr +): FunctionExpr; + +export function substr( + field: Expr | string, + position: Expr | number, + length?: Expr | number +): FunctionExpr { + const fieldExpr = fieldOrExpression(field); + const positionExpr = valueToDefaultExpr(position); + const lengthExpr = + length === undefined ? undefined : valueToDefaultExpr(length); + return fieldExpr.substr(positionExpr, lengthExpr); +} + +/** + * @beta + * + * Creates an expression that adds two expressions together. + * + * ```typescript + * // Add the value of the 'quantity' field and the 'reserve' field. + * add(field("quantity"), field("reserve")); + * ``` + * + * @param first The first expression to add. + * @param second The second expression or literal to add. + * @param others Optional other expressions or literals to add. + * @return A new {@code Expr} representing the addition operation. + */ +export function add( + first: Expr, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that adds a field's value to an expression. + * + * ```typescript + * // Add the value of the 'quantity' field and the 'reserve' field. + * add("quantity", field("reserve")); + * ``` + * + * @param fieldName The name of the field containing the value to add. + * @param second The second expression or literal to add. + * @param others Optional other expressions or literals to add. + * @return A new {@code Expr} representing the addition operation. + */ +export function add( + fieldName: string, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +export function add( + first: Expr | string, + second: Expr | unknown, + ...others: Array +): FunctionExpr { + return fieldOrExpression(first).add( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); +} + +/** + * @beta + * + * Creates an expression that subtracts two expressions. + * + * ```typescript + * // Subtract the 'discount' field from the 'price' field + * subtract(field("price"), field("discount")); + * ``` + * + * @param left The expression to subtract from. + * @param right The expression to subtract. + * @return A new {@code Expr} representing the subtraction operation. + */ +export function subtract(left: Expr, right: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that subtracts a constant value from an expression. + * + * ```typescript + * // Subtract the constant value 2 from the 'value' field + * subtract(field("value"), 2); + * ``` + * + * @param expression The expression to subtract from. + * @param value The constant value to subtract. + * @return A new {@code Expr} representing the subtraction operation. + */ +export function subtract(expression: Expr, value: unknown): FunctionExpr; + +/** + * @beta + * + * Creates an expression that subtracts an expression from a field's value. + * + * ```typescript + * // Subtract the 'discount' field from the 'price' field + * subtract("price", field("discount")); + * ``` + * + * @param fieldName The field name to subtract from. + * @param expression The expression to subtract. + * @return A new {@code Expr} representing the subtraction operation. + */ +export function subtract(fieldName: string, expression: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that subtracts a constant value from a field's value. + * + * ```typescript + * // Subtract 20 from the value of the 'total' field + * subtract("total", 20); + * ``` + * + * @param fieldName The field name to subtract from. + * @param value The constant value to subtract. + * @return A new {@code Expr} representing the subtraction operation. + */ +export function subtract(fieldName: string, value: unknown): FunctionExpr; +export function subtract( + left: Expr | string, + right: Expr | unknown +): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.subtract(normalizedRight); +} + +/** + * @beta + * + * Creates an expression that multiplies two expressions together. + * + * ```typescript + * // Multiply the 'quantity' field by the 'price' field + * multiply(field("quantity"), field("price")); + * ``` + * + * @param first The first expression to multiply. + * @param second The second expression or literal to multiply. + * @param others Optional additional expressions or literals to multiply. + * @return A new {@code Expr} representing the multiplication operation. + */ +export function multiply( + first: Expr, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that multiplies a field's value by an expression. + * + * ```typescript + * // Multiply the 'quantity' field by the 'price' field + * multiply("quantity", field("price")); + * ``` + * + * @param fieldName The name of the field containing the value to add. + * @param second The second expression or literal to add. + * @param others Optional other expressions or literals to add. + * @return A new {@code Expr} representing the multiplication operation. + */ +export function multiply( + fieldName: string, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +export function multiply( + first: Expr | string, + second: Expr | unknown, + ...others: Array +): FunctionExpr { + return fieldOrExpression(first).multiply( + valueToDefaultExpr(second), + ...others.map(valueToDefaultExpr) + ); +} + +/** + * @beta + * + * Creates an expression that divides two expressions. + * + * ```typescript + * // Divide the 'total' field by the 'count' field + * divide(field("total"), field("count")); + * ``` + * + * @param left The expression to be divided. + * @param right The expression to divide by. + * @return A new {@code Expr} representing the division operation. + */ +export function divide(left: Expr, right: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that divides an expression by a constant value. + * + * ```typescript + * // Divide the 'value' field by 10 + * divide(field("value"), 10); + * ``` + * + * @param expression The expression to be divided. + * @param value The constant value to divide by. + * @return A new {@code Expr} representing the division operation. + */ +export function divide(expression: Expr, value: unknown): FunctionExpr; + +/** + * @beta + * + * Creates an expression that divides a field's value by an expression. + * + * ```typescript + * // Divide the 'total' field by the 'count' field + * divide("total", field("count")); + * ``` + * + * @param fieldName The field name to be divided. + * @param expressions The expression to divide by. + * @return A new {@code Expr} representing the division operation. + */ +export function divide(fieldName: string, expressions: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that divides a field's value by a constant value. + * + * ```typescript + * // Divide the 'value' field by 10 + * divide("value", 10); + * ``` + * + * @param fieldName The field name to be divided. + * @param value The constant value to divide by. + * @return A new {@code Expr} representing the division operation. + */ +export function divide(fieldName: string, value: unknown): FunctionExpr; +export function divide( + left: Expr | string, + right: Expr | unknown +): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.divide(normalizedRight); +} + +/** + * @beta + * + * Creates an expression that calculates the modulo (remainder) of dividing two expressions. + * + * ```typescript + * // Calculate the remainder of dividing 'field1' by 'field2'. + * mod(field("field1"), field("field2")); + * ``` + * + * @param left The dividend expression. + * @param right The divisor expression. + * @return A new {@code Expr} representing the modulo operation. + */ +export function mod(left: Expr, right: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the modulo (remainder) of dividing an expression by a constant. + * + * ```typescript + * // Calculate the remainder of dividing 'field1' by 5. + * mod(field("field1"), 5); + * ``` + * + * @param expression The dividend expression. + * @param value The divisor constant. + * @return A new {@code Expr} representing the modulo operation. + */ +export function mod(expression: Expr, value: unknown): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the modulo (remainder) of dividing a field's value by an expression. + * + * ```typescript + * // Calculate the remainder of dividing 'field1' by 'field2'. + * mod("field1", field("field2")); + * ``` + * + * @param fieldName The dividend field name. + * @param expression The divisor expression. + * @return A new {@code Expr} representing the modulo operation. + */ +export function mod(fieldName: string, expression: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the modulo (remainder) of dividing a field's value by a constant. + * + * ```typescript + * // Calculate the remainder of dividing 'field1' by 5. + * mod("field1", 5); + * ``` + * + * @param fieldName The dividend field name. + * @param value The divisor constant. + * @return A new {@code Expr} representing the modulo operation. + */ +export function mod(fieldName: string, value: unknown): FunctionExpr; +export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.mod(normalizedRight); +} + +/** + * @beta + * + * Creates an expression that creates a Firestore map value from an input object. + * + * ```typescript + * // Create a map from the input object and reference the 'baz' field value from the input document. + * map({foo: 'bar', baz: Field.of('baz')}).as('data'); + * ``` + * + * @param elements The input map to evaluate in the expression. + * @return A new {@code Expr} representing the map function. + */ +export function map(elements: Record): FunctionExpr { + const result: Expr[] = []; + for (const key in elements) { + if (Object.prototype.hasOwnProperty.call(elements, key)) { + const value = elements[key]; + result.push(constant(key)); + result.push(valueToDefaultExpr(value)); + } + } + return new FunctionExpr('map', result); +} + +/** + * Internal use only + * Converts a plainObject to a mapValue in the proto representation, + * rather than a functionValue+map that is the result of the map(...) function. + * This behaves different from constant(plainObject) because it + * traverses the input object, converts values in the object to expressions, + * and calls _readUserData on each of these expressions. + * @private + * @internal + * @param plainObject + */ +export function _mapValue(plainObject: Record): MapValue { + const result: Map = new Map(); + for (const key in plainObject) { + if (Object.prototype.hasOwnProperty.call(plainObject, key)) { + const value = plainObject[key]; + result.set(key, valueToDefaultExpr(value)); + } + } + return new MapValue(result); +} + +/** + * @beta + * + * Creates an expression that creates a Firestore array value from an input array. + * + * ```typescript + * // Create an array value from the input array and reference the 'baz' field value from the input document. + * array(['bar', Field.of('baz')]).as('foo'); + * ``` + * + * @param elements The input array to evaluate in the expression. + * @return A new {@code Expr} representing the array function. + */ +export function array(elements: unknown[]): FunctionExpr { + return new FunctionExpr( + 'array', + elements.map(element => valueToDefaultExpr(element)) + ); +} + +/** + * @beta + * + * Creates an expression that checks if two expressions are equal. + * + * ```typescript + * // Check if the 'age' field is equal to an expression + * eq(field("age"), field("minAge").add(10)); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the equality comparison. + */ +export function eq(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is equal to a constant value. + * + * ```typescript + * // Check if the 'age' field is equal to 21 + * eq(field("age"), 21); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the equality comparison. + */ +export function eq(expression: Expr, value: unknown): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is equal to an expression. + * + * ```typescript + * // Check if the 'age' field is equal to the 'limit' field + * eq("age", field("limit")); + * ``` + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new `Expr` representing the equality comparison. + */ +export function eq(fieldName: string, expression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is equal to a constant value. + * + * ```typescript + * // Check if the 'city' field is equal to string constant "London" + * eq("city", "London"); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the equality comparison. + */ +export function eq(fieldName: string, value: unknown): BooleanExpr; +export function eq(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.eq(rightExpr); +} + +/** + * @beta + * + * Creates an expression that checks if two expressions are not equal. + * + * ```typescript + * // Check if the 'status' field is not equal to field 'finalState' + * neq(field("status"), field("finalState")); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the inequality comparison. + */ +export function neq(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is not equal to a constant value. + * + * ```typescript + * // Check if the 'status' field is not equal to "completed" + * neq(field("status"), "completed"); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the inequality comparison. + */ +export function neq(expression: Expr, value: unknown): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is not equal to an expression. + * + * ```typescript + * // Check if the 'status' field is not equal to the value of 'expectedStatus' + * neq("status", field("expectedStatus")); + * ``` + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new `Expr` representing the inequality comparison. + */ +export function neq(fieldName: string, expression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is not equal to a constant value. + * + * ```typescript + * // Check if the 'country' field is not equal to "USA" + * neq("country", "USA"); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the inequality comparison. + */ +export function neq(fieldName: string, value: unknown): BooleanExpr; +export function neq(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.neq(rightExpr); +} + +/** + * @beta + * + * Creates an expression that checks if the first expression is less than the second expression. + * + * ```typescript + * // Check if the 'age' field is less than 30 + * lt(field("age"), field("limit")); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the less than comparison. + */ +export function lt(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is less than a constant value. + * + * ```typescript + * // Check if the 'age' field is less than 30 + * lt(field("age"), 30); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the less than comparison. + */ +export function lt(expression: Expr, value: unknown): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is less than an expression. + * + * ```typescript + * // Check if the 'age' field is less than the 'limit' field + * lt("age", field("limit")); + * ``` + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new `Expr` representing the less than comparison. + */ +export function lt(fieldName: string, expression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is less than a constant value. + * + * ```typescript + * // Check if the 'price' field is less than 50 + * lt("price", 50); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the less than comparison. + */ +export function lt(fieldName: string, value: unknown): BooleanExpr; +export function lt(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lt(rightExpr); +} + +/** + * @beta + * + * Creates an expression that checks if the first expression is less than or equal to the second + * expression. + * + * ```typescript + * // Check if the 'quantity' field is less than or equal to 20 + * lte(field("quantity"), field("limit")); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the less than or equal to comparison. + */ +export function lte(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is less than or equal to a constant value. + * + * ```typescript + * // Check if the 'quantity' field is less than or equal to 20 + * lte(field("quantity"), 20); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the less than or equal to comparison. + */ +export function lte(expression: Expr, value: unknown): BooleanExpr; + +/** + * Creates an expression that checks if a field's value is less than or equal to an expression. + * + * ```typescript + * // Check if the 'quantity' field is less than or equal to the 'limit' field + * lte("quantity", field("limit")); + * ``` + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new `Expr` representing the less than or equal to comparison. + */ +export function lte(fieldName: string, expression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is less than or equal to a constant value. + * + * ```typescript + * // Check if the 'score' field is less than or equal to 70 + * lte("score", 70); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the less than or equal to comparison. + */ +export function lte(fieldName: string, value: unknown): BooleanExpr; +export function lte(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lte(rightExpr); +} + +/** + * @beta + * + * Creates an expression that checks if the first expression is greater than the second + * expression. + * + * ```typescript + * // Check if the 'age' field is greater than 18 + * gt(field("age"), Constant(9).add(9)); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the greater than comparison. + */ +export function gt(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is greater than a constant value. + * + * ```typescript + * // Check if the 'age' field is greater than 18 + * gt(field("age"), 18); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the greater than comparison. + */ +export function gt(expression: Expr, value: unknown): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is greater than an expression. + * + * ```typescript + * // Check if the value of field 'age' is greater than the value of field 'limit' + * gt("age", field("limit")); + * ``` + * + * @param fieldName The field name to compare. + * @param expression The expression to compare to. + * @return A new `Expr` representing the greater than comparison. + */ +export function gt(fieldName: string, expression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is greater than a constant value. + * + * ```typescript + * // Check if the 'price' field is greater than 100 + * gt("price", 100); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the greater than comparison. + */ +export function gt(fieldName: string, value: unknown): BooleanExpr; +export function gt(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.gt(rightExpr); +} + +/** + * @beta + * + * Creates an expression that checks if the first expression is greater than or equal to the + * second expression. + * + * ```typescript + * // Check if the 'quantity' field is greater than or equal to the field "threshold" + * gte(field("quantity"), field("threshold")); + * ``` + * + * @param left The first expression to compare. + * @param right The second expression to compare. + * @return A new `Expr` representing the greater than or equal to comparison. + */ +export function gte(left: Expr, right: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is greater than or equal to a constant + * value. + * + * ```typescript + * // Check if the 'quantity' field is greater than or equal to 10 + * gte(field("quantity"), 10); + * ``` + * + * @param expression The expression to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the greater than or equal to comparison. + */ +export function gte(expression: Expr, value: unknown): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is greater than or equal to an expression. + * + * ```typescript + * // Check if the value of field 'age' is greater than or equal to the value of field 'limit' + * gte("age", field("limit")); + * ``` + * + * @param fieldName The field name to compare. + * @param value The expression to compare to. + * @return A new `Expr` representing the greater than or equal to comparison. + */ +export function gte(fieldName: string, value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is greater than or equal to a constant + * value. + * + * ```typescript + * // Check if the 'score' field is greater than or equal to 80 + * gte("score", 80); + * ``` + * + * @param fieldName The field name to compare. + * @param value The constant value to compare to. + * @return A new `Expr` representing the greater than or equal to comparison. + */ +export function gte(fieldName: string, value: unknown): BooleanExpr; +export function gte(left: Expr | string, right: unknown): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.gte(rightExpr); +} + +/** + * @beta + * + * Creates an expression that concatenates an array expression with other arrays. + * + * ```typescript + * // Combine the 'items' array with two new item arrays + * arrayConcat(field("items"), [field("newItems"), field("otherItems")]); + * ``` + * + * @param firstArray The first array expression to concatenate to. + * @param secondArray The second array expression or array literal to concatenate to. + * @param otherArrays Optional additional array expressions or array literals to concatenate. + * @return A new {@code Expr} representing the concatenated array. + */ +export function arrayConcat( + firstArray: Expr, + secondArray: Expr | unknown[], + ...otherArrays: Array +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that concatenates a field's array value with other arrays. + * + * ```typescript + * // Combine the 'items' array with two new item arrays + * arrayConcat("items", [field("newItems"), field("otherItems")]); + * ``` + * + * @param firstArrayField The first array to concatenate to. + * @param secondArray The second array expression or array literal to concatenate to. + * @param otherArrays Optional additional array expressions or array literals to concatenate. + * @return A new {@code Expr} representing the concatenated array. + */ +export function arrayConcat( + firstArrayField: string, + secondArray: Expr | unknown[], + ...otherArrays: Array +): FunctionExpr; + +export function arrayConcat( + firstArray: Expr | string, + secondArray: Expr | unknown[], + ...otherArrays: Array +): FunctionExpr { + const exprValues = otherArrays.map(element => valueToDefaultExpr(element)); + return fieldOrExpression(firstArray).arrayConcat( + fieldOrExpression(secondArray), + ...exprValues + ); +} + +/** + * @beta + * + * Creates an expression that checks if an array expression contains a specific element. + * + * ```typescript + * // Check if the 'colors' array contains the value of field 'selectedColor' + * arrayContains(field("colors"), field("selectedColor")); + * ``` + * + * @param array The array expression to check. + * @param element The element to search for in the array. + * @return A new {@code Expr} representing the 'array_contains' comparison. + */ +export function arrayContains(array: Expr, element: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that checks if an array expression contains a specific element. + * + * ```typescript + * // Check if the 'colors' array contains "red" + * arrayContains(field("colors"), "red"); + * ``` + * + * @param array The array expression to check. + * @param element The element to search for in the array. + * @return A new {@code Expr} representing the 'array_contains' comparison. + */ +export function arrayContains(array: Expr, element: unknown): FunctionExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains a specific element. + * + * ```typescript + * // Check if the 'colors' array contains the value of field 'selectedColor' + * arrayContains("colors", field("selectedColor")); + * ``` + * + * @param fieldName The field name to check. + * @param element The element to search for in the array. + * @return A new {@code Expr} representing the 'array_contains' comparison. + */ +export function arrayContains(fieldName: string, element: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains a specific value. + * + * ```typescript + * // Check if the 'colors' array contains "red" + * arrayContains("colors", "red"); + * ``` + * + * @param fieldName The field name to check. + * @param element The element to search for in the array. + * @return A new {@code Expr} representing the 'array_contains' comparison. + */ +export function arrayContains(fieldName: string, element: unknown): BooleanExpr; +export function arrayContains( + array: Expr | string, + element: unknown +): BooleanExpr { + const arrayExpr = fieldOrExpression(array); + const elementExpr = valueToDefaultExpr(element); + return arrayExpr.arrayContains(elementExpr); +} + +/** + * @beta + * + * Creates an expression that checks if an array expression contains any of the specified + * elements. + * + * ```typescript + * // Check if the 'categories' array contains either values from field "cate1" or "Science" + * arrayContainsAny(field("categories"), [field("cate1"), "Science"]); + * ``` + * + * @param array The array expression to check. + * @param values The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_any' comparison. + */ +export function arrayContainsAny( + array: Expr, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains any of the specified + * elements. + * + * ```typescript + * // Check if the 'groups' array contains either the value from the 'userGroup' field + * // or the value "guest" + * arrayContainsAny("categories", [field("cate1"), "Science"]); + * ``` + * + * @param fieldName The field name to check. + * @param values The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_any' comparison. + */ +export function arrayContainsAny( + fieldName: string, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an array expression contains any of the specified + * elements. + * + * ```typescript + * // Check if the 'categories' array contains either values from field "cate1" or "Science" + * arrayContainsAny(field("categories"), array([field("cate1"), "Science"])); + * ``` + * + * @param array The array expression to check. + * @param values An expression that evaluates to an array, whose elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_any' comparison. + */ +export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains any of the specified + * elements. + * + * ```typescript + * // Check if the 'groups' array contains either the value from the 'userGroup' field + * // or the value "guest" + * arrayContainsAny("categories", array([field("cate1"), "Science"])); + * ``` + * + * @param fieldName The field name to check. + * @param values An expression that evaluates to an array, whose elements to check for in the array field. + * @return A new {@code Expr} representing the 'array_contains_any' comparison. + */ +export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; +export function arrayContainsAny( + array: Expr | string, + values: unknown[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOrExpression(array).arrayContainsAny(values); +} + +/** + * @beta + * + * Creates an expression that checks if an array expression contains all the specified elements. + * + * ```typescript + * // Check if the "tags" array contains all of the values: "SciFi", "Adventure", and the value from field "tag1" + * arrayContainsAll(field("tags"), [field("tag1"), constant("SciFi"), "Adventure"]); + * ``` + * + * @param array The array expression to check. + * @param values The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_all' comparison. + */ +export function arrayContainsAll( + array: Expr, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains all the specified values or + * expressions. + * + * ```typescript + * // Check if the 'tags' array contains both of the values from field 'tag1', the value "SciFi", and "Adventure" + * arrayContainsAll("tags", [field("tag1"), "SciFi", "Adventure"]); + * ``` + * + * @param fieldName The field name to check. + * @param values The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_all' comparison. + */ +export function arrayContainsAll( + fieldName: string, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an array expression contains all the specified elements. + * + * ```typescript + * // Check if the "tags" array contains all of the values: "SciFi", "Adventure", and the value from field "tag1" + * arrayContainsAll(field("tags"), [field("tag1"), constant("SciFi"), "Adventure"]); + * ``` + * + * @param array The array expression to check. + * @param arrayExpression The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_all' comparison. + */ +export function arrayContainsAll( + array: Expr, + arrayExpression: Expr +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's array value contains all the specified values or + * expressions. + * + * ```typescript + * // Check if the 'tags' array contains both of the values from field 'tag1', the value "SciFi", and "Adventure" + * arrayContainsAll("tags", [field("tag1"), "SciFi", "Adventure"]); + * ``` + * + * @param fieldName The field name to check. + * @param arrayExpression The elements to check for in the array. + * @return A new {@code Expr} representing the 'array_contains_all' comparison. + */ +export function arrayContainsAll( + fieldName: string, + arrayExpression: Expr +): BooleanExpr; +export function arrayContainsAll( + array: Expr | string, + values: unknown[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOrExpression(array).arrayContainsAll(values); +} + +/** + * @beta + * + * Creates an expression that calculates the length of an array in a specified field. + * + * ```typescript + * // Get the number of items in field 'cart' + * arrayLength('cart'); + * ``` + * + * @param fieldName The name of the field containing an array to calculate the length of. + * @return A new {@code Expr} representing the length of the array. + */ +export function arrayLength(fieldName: string): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the length of an array expression. + * + * ```typescript + * // Get the number of items in the 'cart' array + * arrayLength(field("cart")); + * ``` + * + * @param array The array expression to calculate the length of. + * @return A new {@code Expr} representing the length of the array. + */ +export function arrayLength(array: Expr): FunctionExpr; +export function arrayLength(array: Expr | string): FunctionExpr { + return fieldOrExpression(array).arrayLength(); +} + +/** + * @beta + * + * Creates an expression that checks if an expression, when evaluated, is equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' + * eqAny(field("category"), [constant("Electronics"), field("primaryType")]); + * ``` + * + * @param expression The expression whose results to compare. + * @param values The values to check against. + * @return A new {@code Expr} representing the 'IN' comparison. + */ +export function eqAny( + expression: Expr, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is equal to any of the provided values. + * + * ```typescript + * // Check if the 'category' field is set to a value in the disabledCategories field + * eqAny(field("category"), field('disabledCategories')); + * ``` + * + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input. + * @return A new {@code Expr} representing the 'IN' comparison. + */ +export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' + * eqAny("category", [constant("Electronics"), field("primaryType")]); + * ``` + * + * @param fieldName The field to compare. + * @param values The values to check against. + * @return A new {@code Expr} representing the 'IN' comparison. + */ +export function eqAny( + fieldName: string, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' + * eqAny("category", ["Electronics", field("primaryType")]); + * ``` + * + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input field. + * @return A new {@code Expr} representing the 'IN' comparison. + */ +export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; +export function eqAny( + element: Expr | string, + values: unknown[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOrExpression(element).eqAny(values); +} + +/** + * @beta + * + * Creates an expression that checks if an expression is not equal to any of the provided values + * or expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' + * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * ``` + * + * @param element The expression to compare. + * @param values The values to check against. + * @return A new {@code Expr} representing the 'NOT IN' comparison. + */ +export function notEqAny( + element: Expr, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is not equal to any of the provided values + * or expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' + * notEqAny("status", [constant("pending"), field("rejectedStatus")]); + * ``` + * + * @param fieldName The field name to compare. + * @param values The values to check against. + * @return A new {@code Expr} representing the 'NOT IN' comparison. + */ +export function notEqAny( + fieldName: string, + values: Array +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if an expression is not equal to any of the provided values + * or expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of the field 'rejectedStatus' + * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * ``` + * + * @param element The expression to compare. + * @param arrayExpression The values to check against. + * @return A new {@code Expr} representing the 'NOT IN' comparison. + */ +export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value is not equal to any of the values in the evaluated expression. + * + * ```typescript + * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' + * notEqAny("status", field("rejectedStatuses")); + * ``` + * + * @param fieldName The field name to compare. + * @param arrayExpression The values to check against. + * @return A new {@code Expr} representing the 'NOT IN' comparison. + */ +export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +export function notEqAny( + element: Expr | string, + values: unknown[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOrExpression(element).notEqAny(values); +} + +/** + * @beta + * + * Creates an expression that performs a logical 'XOR' (exclusive OR) operation on multiple BooleanExpressions. + * + * ```typescript + * // Check if only one of the conditions is true: 'age' greater than 18, 'city' is "London", + * // or 'status' is "active". + * const condition = xor( + * gt("age", 18), + * eq("city", "London"), + * eq("status", "active")); + * ``` + * + * @param first The first condition. + * @param second The second condition. + * @param additionalConditions Additional conditions to 'XOR' together. + * @return A new {@code Expr} representing the logical 'XOR' operation. + */ +export function xor( + first: BooleanExpr, + second: BooleanExpr, + ...additionalConditions: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('xor', [first, second, ...additionalConditions]); +} + +/** + * @beta + * + * Creates a conditional expression that evaluates to a 'then' expression if a condition is true + * and an 'else' expression if the condition is false. + * + * ```typescript + * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". + * cond( + * gt("age", 18), constant("Adult"), constant("Minor")); + * ``` + * + * @param condition The condition to evaluate. + * @param thenExpr The expression to evaluate if the condition is true. + * @param elseExpr The expression to evaluate if the condition is false. + * @return A new {@code Expr} representing the conditional expression. + */ +export function cond( + condition: BooleanExpr, + thenExpr: Expr, + elseExpr: Expr +): FunctionExpr { + return new FunctionExpr('cond', [condition, thenExpr, elseExpr]); +} + +/** + * @beta + * + * Creates an expression that negates a filter condition. + * + * ```typescript + * // Find documents where the 'completed' field is NOT true + * not(eq("completed", true)); + * ``` + * + * @param booleanExpr The filter condition to negate. + * @return A new {@code Expr} representing the negated filter condition. + */ +export function not(booleanExpr: BooleanExpr): BooleanExpr { + return booleanExpr.not(); +} + +/** + * @beta + * + * Creates an expression that returns the largest value between multiple input + * expressions or literal values. Based on Firestore's value type ordering. + * + * ```typescript + * // Returns the largest value between the 'field1' field, the 'field2' field, + * // and 1000 + * logicalMaximum(field("field1"), field("field2"), 1000); + * ``` + * + * @param first The first operand expression. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. + * @return A new {@code Expr} representing the logical max operation. + */ +export function logicalMaximum( + first: Expr, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns the largest value between multiple input + * expressions or literal values. Based on Firestore's value type ordering. + * + * ```typescript + * // Returns the largest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMaximum("field1", field("field2"), 1000); + * ``` + * + * @param fieldName The first operand field name. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. + * @return A new {@code Expr} representing the logical max operation. + */ +export function logicalMaximum( + fieldName: string, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +export function logicalMaximum( + first: Expr | string, + second: Expr | unknown, + ...others: Array +): FunctionExpr { + return fieldOrExpression(first).logicalMaximum( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); +} + +/** + * @beta + * + * Creates an expression that returns the smallest value between multiple input + * expressions and literal values. Based on Firestore's value type ordering. + * + * ```typescript + * // Returns the smallest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMinimum(field("field1"), field("field2"), 1000); + * ``` + * + * @param first The first operand expression. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. + * @return A new {@code Expr} representing the logical min operation. + */ +export function logicalMinimum( + first: Expr, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that returns the smallest value between a field's value + * and other input expressions or literal values. + * Based on Firestore's value type ordering. + * + * ```typescript + * // Returns the smallest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMinimum("field1", field("field2"), 1000); + * ``` + * + * @param fieldName The first operand field name. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. + * @return A new {@code Expr} representing the logical min operation. + */ +export function logicalMinimum( + fieldName: string, + second: Expr | unknown, + ...others: Array +): FunctionExpr; + +export function logicalMinimum( + first: Expr | string, + second: Expr | unknown, + ...others: Array +): FunctionExpr { + return fieldOrExpression(first).logicalMinimum( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); +} + +/** + * @beta + * + * Creates an expression that checks if a field exists. + * + * ```typescript + * // Check if the document has a field named "phoneNumber" + * exists(field("phoneNumber")); + * ``` + * + * @param value An expression evaluates to the name of the field to check. + * @return A new {@code Expr} representing the 'exists' check. + */ +export function exists(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field exists. + * + * ```typescript + * // Check if the document has a field named "phoneNumber" + * exists("phoneNumber"); + * ``` + * + * @param fieldName The field name to check. + * @return A new {@code Expr} representing the 'exists' check. + */ +export function exists(fieldName: string): BooleanExpr; +export function exists(valueOrField: Expr | string): BooleanExpr { + return fieldOrExpression(valueOrField).exists(); +} + +/** + * @beta + * + * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * isNaN(field("value").divide(0)); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNan(value: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * isNaN("value"); + * ``` + * + * @param fieldName The name of the field to check. + * @return A new {@code Expr} representing the 'isNaN' check. + */ +export function isNan(fieldName: string): BooleanExpr; +export function isNan(value: Expr | string): BooleanExpr { + return fieldOrExpression(value).isNan(); +} + +/** + * @beta + * + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * reverse(field("myString")); + * ``` + * + * @param stringExpression An expression evaluating to a string value, which will be reversed. + * @return A new {@code Expr} representing the reversed string. + */ +export function reverse(stringExpression: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that reverses a string value in the specified field. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * reverse("myString"); + * ``` + * + * @param field The name of the field representing the string to reverse. + * @return A new {@code Expr} representing the reversed string. + */ +export function reverse(field: string): FunctionExpr; +export function reverse(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).reverse(); +} + +/** + * @beta + * + * Creates an expression that replaces the first occurrence of a substring within a string with another substring. + * + * ```typescript + * // Replace the first occurrence of "hello" with "hi" in the 'message' field. + * replaceFirst(field("message"), "hello", "hi"); + * ``` + * + * @param value The expression representing the string to perform the replacement on. + * @param find The substring to search for. + * @param replace The substring to replace the first occurrence of 'find' with. + * @return A new {@code Expr} representing the string with the first occurrence replaced. + */ +export function replaceFirst( + value: Expr, + find: string, + replace: string +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that replaces the first occurrence of a substring within a string with another substring, + * where the substring to find and the replacement substring are specified by expressions. + * + * ```typescript + * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field. + * replaceFirst(field("message"), field("findField"), field("replaceField")); + * ``` + * + * @param value The expression representing the string to perform the replacement on. + * @param find The expression representing the substring to search for. + * @param replace The expression representing the substring to replace the first occurrence of 'find' with. + * @return A new {@code Expr} representing the string with the first occurrence replaced. + */ +export function replaceFirst( + value: Expr, + find: Expr, + replace: Expr +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that replaces the first occurrence of a substring within a string represented by a field with another substring. + * + * ```typescript + * // Replace the first occurrence of "hello" with "hi" in the 'message' field. + * replaceFirst("message", "hello", "hi"); + * ``` + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The substring to search for. + * @param replace The substring to replace the first occurrence of 'find' with. + * @return A new {@code Expr} representing the string with the first occurrence replaced. + */ +export function replaceFirst( + fieldName: string, + find: string, + replace: string +): FunctionExpr; +export function replaceFirst( + value: Expr | string, + find: Expr | string, + replace: Expr | string +): FunctionExpr { + const normalizedValue = fieldOrExpression(value); + const normalizedFind = valueToDefaultExpr(find); + const normalizedReplace = valueToDefaultExpr(replace); + return normalizedValue.replaceFirst(normalizedFind, normalizedReplace); +} + +/** + * @beta + * + * Creates an expression that replaces all occurrences of a substring within a string with another substring. + * + * ```typescript + * // Replace all occurrences of "hello" with "hi" in the 'message' field. + * replaceAll(field("message"), "hello", "hi"); + * ``` + * + * @param value The expression representing the string to perform the replacement on. + * @param find The substring to search for. + * @param replace The substring to replace all occurrences of 'find' with. + * @return A new {@code Expr} representing the string with all occurrences replaced. + */ +export function replaceAll( + value: Expr, + find: string, + replace: string +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that replaces all occurrences of a substring within a string with another substring, + * where the substring to find and the replacement substring are specified by expressions. + * + * ```typescript + * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field. + * replaceAll(field("message"), field("findField"), field("replaceField")); + * ``` + * + * @param value The expression representing the string to perform the replacement on. + * @param find The expression representing the substring to search for. + * @param replace The expression representing the substring to replace all occurrences of 'find' with. + * @return A new {@code Expr} representing the string with all occurrences replaced. + */ +export function replaceAll( + value: Expr, + find: Expr, + replace: Expr +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that replaces all occurrences of a substring within a string represented by a field with another substring. + * + * ```typescript + * // Replace all occurrences of "hello" with "hi" in the 'message' field. + * replaceAll("message", "hello", "hi"); + * ``` + * + * @param fieldName The name of the field representing the string to perform the replacement on. + * @param find The substring to search for. + * @param replace The substring to replace all occurrences of 'find' with. + * @return A new {@code Expr} representing the string with all occurrences replaced. + */ +export function replaceAll( + fieldName: string, + find: string, + replace: string +): FunctionExpr; +export function replaceAll( + value: Expr | string, + find: Expr | string, + replace: Expr | string +): FunctionExpr { + const normalizedValue = fieldOrExpression(value); + const normalizedFind = valueToDefaultExpr(find); + const normalizedReplace = valueToDefaultExpr(replace); + return normalizedValue.replaceAll(normalizedFind, normalizedReplace); +} + +/** + * @beta + * + * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. + * + * ```typescript + * // Calculate the length of the 'myString' field in bytes. + * byteLength(field("myString")); + * ``` + * + * @param expr The expression representing the string. + * @return A new {@code Expr} representing the length of the string in bytes. + */ +export function byteLength(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the length of a string represented by a field in UTF-8 bytes, or just the length of a Blob. + * + * ```typescript + * // Calculate the length of the 'myString' field in bytes. + * byteLength("myString"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the length of the string in bytes. + */ +export function byteLength(fieldName: string): FunctionExpr; +export function byteLength(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.byteLength(); +} + +/** + * @beta + * + * Creates an expression that calculates the character length of a string field in UTF8. + * + * ```typescript + * // Get the character length of the 'name' field in UTF-8. + * strLength("name"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the length of the string. + */ +export function charLength(fieldName: string): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the character length of a string expression in UTF-8. + * + * ```typescript + * // Get the character length of the 'name' field in UTF-8. + * strLength(field("name")); + * ``` + * + * @param stringExpression The expression representing the string to calculate the length of. + * @return A new {@code Expr} representing the length of the string. + */ +export function charLength(stringExpression: Expr): FunctionExpr; +export function charLength(value: Expr | string): FunctionExpr { + const valueExpr = fieldOrExpression(value); + return valueExpr.charLength(); +} + +/** + * @beta + * + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * ```typescript + * // Check if the 'title' field contains the string "guide" + * like("title", "%guide%"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@code Expr} representing the 'like' comparison. + */ +export function like(fieldName: string, pattern: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * ```typescript + * // Check if the 'title' field contains the string "guide" + * like("title", field("pattern")); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@code Expr} representing the 'like' comparison. + */ +export function like(fieldName: string, pattern: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * ```typescript + * // Check if the 'title' field contains the string "guide" + * like(field("title"), "%guide%"); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@code Expr} representing the 'like' comparison. + */ +export function like(stringExpression: Expr, pattern: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * ```typescript + * // Check if the 'title' field contains the string "guide" + * like(field("title"), field("pattern")); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@code Expr} representing the 'like' comparison. + */ +export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; +export function like( + left: Expr | string, + pattern: Expr | string +): FunctionExpr { + const leftExpr = fieldOrExpression(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.like(patternExpr); +} + +/** + * @beta + * + * Creates an expression that checks if a string field contains a specified regular expression as + * a substring. + * + * ```typescript + * // Check if the 'description' field contains "example" (case-insensitive) + * regexContains("description", "(?i)example"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function regexContains(fieldName: string, pattern: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string field contains a specified regular expression as + * a substring. + * + * ```typescript + * // Check if the 'description' field contains "example" (case-insensitive) + * regexContains("description", field("pattern")); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * ```typescript + * // Check if the 'description' field contains "example" (case-insensitive) + * regexContains(field("description"), "(?i)example"); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function regexContains( + stringExpression: Expr, + pattern: string +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * ```typescript + * // Check if the 'description' field contains "example" (case-insensitive) + * regexContains(field("description"), field("pattern")); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function regexContains( + stringExpression: Expr, + pattern: Expr +): BooleanExpr; +export function regexContains( + left: Expr | string, + pattern: Expr | string +): BooleanExpr { + const leftExpr = fieldOrExpression(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.regexContains(patternExpr); +} + +/** + * @beta + * + * Creates an expression that checks if a string field matches a specified regular expression. + * + * ```typescript + * // Check if the 'email' field matches a valid email pattern + * regexMatch("email", "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new {@code Expr} representing the regular expression match. + */ +export function regexMatch(fieldName: string, pattern: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string field matches a specified regular expression. + * + * ```typescript + * // Check if the 'email' field matches a valid email pattern + * regexMatch("email", field("pattern")); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new {@code Expr} representing the regular expression match. + */ +export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression matches a specified regular + * expression. + * + * ```typescript + * // Check if the 'email' field matches a valid email pattern + * regexMatch(field("email"), "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); + * ``` + * + * @param stringExpression The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new {@code Expr} representing the regular expression match. + */ +export function regexMatch( + stringExpression: Expr, + pattern: string +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression matches a specified regular + * expression. + * + * ```typescript + * // Check if the 'email' field matches a valid email pattern + * regexMatch(field("email"), field("pattern")); + * ``` + * + * @param stringExpression The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new {@code Expr} representing the regular expression match. + */ +export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; +export function regexMatch( + left: Expr | string, + pattern: Expr | string +): BooleanExpr { + const leftExpr = fieldOrExpression(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.regexMatch(patternExpr); +} + +/** + * @beta + * + * Creates an expression that checks if a string field contains a specified substring. + * + * ```typescript + * // Check if the 'description' field contains "example". + * strContains("description", "example"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param substring The substring to search for. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function strContains(fieldName: string, substring: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string field contains a substring specified by an expression. + * + * ```typescript + * // Check if the 'description' field contains the value of the 'keyword' field. + * strContains("description", field("keyword")); + * ``` + * + * @param fieldName The name of the field containing the string. + * @param substring The expression representing the substring to search for. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function strContains(fieldName: string, substring: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression contains a specified substring. + * + * ```typescript + * // Check if the 'description' field contains "example". + * strContains(field("description"), "example"); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param substring The substring to search for. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function strContains( + stringExpression: Expr, + substring: string +): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression contains a substring specified by another expression. + * + * ```typescript + * // Check if the 'description' field contains the value of the 'keyword' field. + * strContains(field("description"), field("keyword")); + * ``` + * + * @param stringExpression The expression representing the string to perform the comparison on. + * @param substring The expression representing the substring to search for. + * @return A new {@code Expr} representing the 'contains' comparison. + */ +export function strContains( + stringExpression: Expr, + substring: Expr +): BooleanExpr; +export function strContains( + left: Expr | string, + substring: Expr | string +): BooleanExpr { + const leftExpr = fieldOrExpression(left); + const substringExpr = valueToDefaultExpr(substring); + return leftExpr.strContains(substringExpr); +} + +/** + * @beta + * + * Creates an expression that checks if a field's value starts with a given prefix. + * + * ```typescript + * // Check if the 'name' field starts with "Mr." + * startsWith("name", "Mr."); + * ``` + * + * @param fieldName The field name to check. + * @param prefix The prefix to check for. + * @return A new {@code Expr} representing the 'starts with' comparison. + */ +export function startsWith(fieldName: string, prefix: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value starts with a given prefix. + * + * ```typescript + * // Check if the 'fullName' field starts with the value of the 'firstName' field + * startsWith("fullName", field("firstName")); + * ``` + * + * @param fieldName The field name to check. + * @param prefix The expression representing the prefix. + * @return A new {@code Expr} representing the 'starts with' comparison. + */ +export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression starts with a given prefix. + * + * ```typescript + * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." + * startsWith(field("fullName"), "Mr."); + * ``` + * + * @param stringExpression The expression to check. + * @param prefix The prefix to check for. + * @return A new {@code Expr} representing the 'starts with' comparison. + */ +export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression starts with a given prefix. + * + * ```typescript + * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." + * startsWith(field("fullName"), field("prefix")); + * ``` + * + * @param stringExpression The expression to check. + * @param prefix The prefix to check for. + * @return A new {@code Expr} representing the 'starts with' comparison. + */ +export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; +export function startsWith( + expr: Expr | string, + prefix: Expr | string +): BooleanExpr { + return fieldOrExpression(expr).startsWith(valueToDefaultExpr(prefix)); +} + +/** + * @beta + * + * Creates an expression that checks if a field's value ends with a given postfix. + * + * ```typescript + * // Check if the 'filename' field ends with ".txt" + * endsWith("filename", ".txt"); + * ``` + * + * @param fieldName The field name to check. + * @param suffix The postfix to check for. + * @return A new {@code Expr} representing the 'ends with' comparison. + */ +export function endsWith(fieldName: string, suffix: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a field's value ends with a given postfix. + * + * ```typescript + * // Check if the 'url' field ends with the value of the 'extension' field + * endsWith("url", field("extension")); + * ``` + * + * @param fieldName The field name to check. + * @param suffix The expression representing the postfix. + * @return A new {@code Expr} representing the 'ends with' comparison. + */ +export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression ends with a given postfix. + * + * ```typescript + * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." + * endsWith(field("fullName"), "Jr."); + * ``` + * + * @param stringExpression The expression to check. + * @param suffix The postfix to check for. + * @return A new {@code Expr} representing the 'ends with' comparison. + */ +export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; + +/** + * @beta + * + * Creates an expression that checks if a string expression ends with a given postfix. + * + * ```typescript + * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." + * endsWith(field("fullName"), constant("Jr.")); + * ``` + * + * @param stringExpression The expression to check. + * @param suffix The postfix to check for. + * @return A new {@code Expr} representing the 'ends with' comparison. + */ +export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; +export function endsWith( + expr: Expr | string, + suffix: Expr | string +): BooleanExpr { + return fieldOrExpression(expr).endsWith(valueToDefaultExpr(suffix)); +} + +/** + * @beta + * + * Creates an expression that converts a string field to lowercase. + * + * ```typescript + * // Convert the 'name' field to lowercase + * toLower("name"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the lowercase string. + */ +export function toLower(fieldName: string): FunctionExpr; + +/** + * @beta + * + * Creates an expression that converts a string expression to lowercase. + * + * ```typescript + * // Convert the 'name' field to lowercase + * toLower(field("name")); + * ``` + * + * @param stringExpression The expression representing the string to convert to lowercase. + * @return A new {@code Expr} representing the lowercase string. + */ +export function toLower(stringExpression: Expr): FunctionExpr; +export function toLower(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).toLower(); +} + +/** + * @beta + * + * Creates an expression that converts a string field to uppercase. + * + * ```typescript + * // Convert the 'title' field to uppercase + * toUpper("title"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the uppercase string. + */ +export function toUpper(fieldName: string): FunctionExpr; + +/** + * @beta + * + * Creates an expression that converts a string expression to uppercase. + * + * ```typescript + * // Convert the 'title' field to uppercase + * toUppercase(field("title")); + * ``` + * + * @param stringExpression The expression representing the string to convert to uppercase. + * @return A new {@code Expr} representing the uppercase string. + */ +export function toUpper(stringExpression: Expr): FunctionExpr; +export function toUpper(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).toUpper(); +} + +/** + * @beta + * + * Creates an expression that removes leading and trailing whitespace from a string field. + * + * ```typescript + * // Trim whitespace from the 'userInput' field + * trim("userInput"); + * ``` + * + * @param fieldName The name of the field containing the string. + * @return A new {@code Expr} representing the trimmed string. + */ +export function trim(fieldName: string): FunctionExpr; + +/** + * @beta + * + * Creates an expression that removes leading and trailing whitespace from a string expression. + * + * ```typescript + * // Trim whitespace from the 'userInput' field + * trim(field("userInput")); + * ``` + * + * @param stringExpression The expression representing the string to trim. + * @return A new {@code Expr} representing the trimmed string. + */ +export function trim(stringExpression: Expr): FunctionExpr; +export function trim(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).trim(); +} + +/** + * @beta + * + * Creates an expression that concatenates string functions, fields or constants together. + * + * ```typescript + * // Combine the 'firstName', " ", and 'lastName' fields into a single string + * strConcat("firstName", " ", field("lastName")); + * ``` + * + * @param fieldName The field name containing the initial string value. + * @param secondString An expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. + * @return A new {@code Expr} representing the concatenated string. + */ +export function strConcat( + fieldName: string, + secondString: Expr | string, + ...otherStrings: Array +): FunctionExpr; + +/** + * @beta + * Creates an expression that concatenates string expressions together. + * + * ```typescript + * // Combine the 'firstName', " ", and 'lastName' fields into a single string + * strConcat(field("firstName"), " ", field("lastName")); + * ``` + * + * @param firstString The initial string expression to concatenate to. + * @param secondString An expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. + * @return A new {@code Expr} representing the concatenated string. + */ +export function strConcat( + firstString: Expr, + secondString: Expr | string, + ...otherStrings: Array +): FunctionExpr; +export function strConcat( + first: string | Expr, + second: string | Expr, + ...elements: Array +): FunctionExpr { + return fieldOrExpression(first).strConcat( + valueToDefaultExpr(second), + ...elements.map(valueToDefaultExpr) + ); +} + +/** + * @beta + * + * Accesses a value from a map (object) field using the provided key. + * + * ```typescript + * // Get the 'city' value from the 'address' map field + * mapGet("address", "city"); + * ``` + * + * @param fieldName The field name of the map field. + * @param subField The key to access in the map. + * @return A new {@code Expr} representing the value associated with the given key in the map. + */ +export function mapGet(fieldName: string, subField: string): FunctionExpr; + +/** + * @beta + * + * Accesses a value from a map (object) expression using the provided key. + * + * ```typescript + * // Get the 'city' value from the 'address' map field + * mapGet(field("address"), "city"); + * ``` + * + * @param mapExpression The expression representing the map. + * @param subField The key to access in the map. + * @return A new {@code Expr} representing the value associated with the given key in the map. + */ +export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; +export function mapGet( + fieldOrExpr: string | Expr, + subField: string +): FunctionExpr { + return fieldOrExpression(fieldOrExpr).mapGet(subField); +} + +/** + * @beta + * + * Creates an aggregation that counts the total number of stage inputs. + * + * ```typescript + * // Count the total number of input documents + * countAll().as("totalDocument"); + * ``` + * + * @return A new {@code AggregateFunction} representing the 'countAll' aggregation. + */ +export function countAll(): AggregateFunction { + return new AggregateFunction('count', []); +} + +/** + * @beta + * + * Creates an aggregation that counts the number of stage inputs with valid evaluations of the + * provided expression. + * + * ```typescript + * // Count the number of items where the price is greater than 10 + * count(field("price").gt(10)).as("expensiveItemCount"); + * ``` + * + * @param expression The expression to count. + * @return A new {@code AggregateFunction} representing the 'count' aggregation. + */ +export function count(expression: Expr): AggregateFunction; + +/** + * Creates an aggregation that counts the number of stage inputs where the input field exists. + * + * ```typescript + * // Count the total number of products + * count("productId").as("totalProducts"); + * ``` + * + * @param fieldName The name of the field to count. + * @return A new {@code AggregateFunction} representing the 'count' aggregation. + */ +export function count(fieldName: string): AggregateFunction; +export function count(value: Expr | string): AggregateFunction { + return fieldOrExpression(value).count(); +} + +/** + * @beta + * + * Creates an aggregation that calculates the sum of values from an expression across multiple + * stage inputs. + * + * ```typescript + * // Calculate the total revenue from a set of orders + * sum(field("orderAmount")).as("totalRevenue"); + * ``` + * + * @param expression The expression to sum up. + * @return A new {@code AggregateFunction} representing the 'sum' aggregation. + */ +export function sum(expression: Expr): AggregateFunction; + +/** + * @beta + * + * Creates an aggregation that calculates the sum of a field's values across multiple stage + * inputs. + * + * ```typescript + * // Calculate the total revenue from a set of orders + * sum("orderAmount").as("totalRevenue"); + * ``` + * + * @param fieldName The name of the field containing numeric values to sum up. + * @return A new {@code AggregateFunction} representing the 'sum' aggregation. + */ +export function sum(fieldName: string): AggregateFunction; +export function sum(value: Expr | string): AggregateFunction { + return fieldOrExpression(value).sum(); +} + +/** + * @beta + * + * Creates an aggregation that calculates the average (mean) of values from an expression across + * multiple stage inputs. + * + * ```typescript + * // Calculate the average age of users + * avg(field("age")).as("averageAge"); + * ``` + * + * @param expression The expression representing the values to average. + * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + */ +export function avg(expression: Expr): AggregateFunction; + +/** + * @beta + * + * Creates an aggregation that calculates the average (mean) of a field's values across multiple + * stage inputs. + * + * ```typescript + * // Calculate the average age of users + * avg("age").as("averageAge"); + * ``` + * + * @param fieldName The name of the field containing numeric values to average. + * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + */ +export function avg(fieldName: string): AggregateFunction; +export function avg(value: Expr | string): AggregateFunction { + return fieldOrExpression(value).avg(); +} + +/** + * @beta + * + * Creates an aggregation that finds the minimum value of an expression across multiple stage + * inputs. + * + * ```typescript + * // Find the lowest price of all products + * minimum(field("price")).as("lowestPrice"); + * ``` + * + * @param expression The expression to find the minimum value of. + * @return A new {@code AggregateFunction} representing the 'min' aggregation. + */ +export function minimum(expression: Expr): AggregateFunction; + +/** + * @beta + * + * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. + * + * ```typescript + * // Find the lowest price of all products + * minimum("price").as("lowestPrice"); + * ``` + * + * @param fieldName The name of the field to find the minimum value of. + * @return A new {@code AggregateFunction} representing the 'min' aggregation. + */ +export function minimum(fieldName: string): AggregateFunction; +export function minimum(value: Expr | string): AggregateFunction { + return fieldOrExpression(value).minimum(); +} + +/** + * @beta + * + * Creates an aggregation that finds the maximum value of an expression across multiple stage + * inputs. + * + * ```typescript + * // Find the highest score in a leaderboard + * maximum(field("score")).as("highestScore"); + * ``` + * + * @param expression The expression to find the maximum value of. + * @return A new {@code AggregateFunction} representing the 'max' aggregation. + */ +export function maximum(expression: Expr): AggregateFunction; + +/** + * @beta + * + * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. + * + * ```typescript + * // Find the highest score in a leaderboard + * maximum("score").as("highestScore"); + * ``` + * + * @param fieldName The name of the field to find the maximum value of. + * @return A new {@code AggregateFunction} representing the 'max' aggregation. + */ +export function maximum(fieldName: string): AggregateFunction; +export function maximum(value: Expr | string): AggregateFunction { + return fieldOrExpression(value).maximum(); +} + +/** + * @beta + * + * Calculates the Cosine distance between a field's vector value and a literal vector value. + * + * ```typescript + * // Calculate the Cosine distance between the 'location' field and a target location + * cosineDistance("location", [37.7749, -122.4194]); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles) or {@link VectorValue} to compare against. + * @return A new {@code Expr} representing the Cosine distance between the two vectors. + */ +export function cosineDistance( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; + +/** + * @beta + * + * Calculates the Cosine distance between a field's vector value and a vector expression. + * + * ```typescript + * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field + * cosineDistance("userVector", field("itemVector")); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vectorExpression The other vector (represented as an Expr) to compare against. + * @return A new {@code Expr} representing the cosine distance between the two vectors. + */ +export function cosineDistance( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; + +/** + * @beta + * + * Calculates the Cosine distance between a vector expression and a vector literal. + * + * ```typescript + * // Calculate the cosine distance between the 'location' field and a target location + * cosineDistance(field("location"), [37.7749, -122.4194]); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to compare against. + * @param vector The other vector (as an array of doubles or VectorValue) to compare against. + * @return A new {@code Expr} representing the cosine distance between the two vectors. + */ +export function cosineDistance( + vectorExpression: Expr, + vector: number[] | Expr +): FunctionExpr; + +/** + * @beta + * + * Calculates the Cosine distance between two vector expressions. + * + * ```typescript + * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field + * cosineDistance(field("userVector"), field("itemVector")); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to compare against. + * @param otherVectorExpression The other vector (represented as an Expr) to compare against. + * @return A new {@code Expr} representing the cosine distance between the two vectors. + */ +export function cosineDistance( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function cosineDistance( + expr: Expr | string, + other: Expr | number[] | VectorValue +): FunctionExpr { + const expr1 = fieldOrExpression(expr); + const expr2 = vectorToExpr(other); + return expr1.cosineDistance(expr2); +} + +/** + * @beta + * + * Calculates the dot product between a field's vector value and a double array. + * + * ```typescript + * // Calculate the dot product distance between a feature vector and a target vector + * dotProduct("features", [0.5, 0.8, 0.2]); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles or VectorValue) to calculate with. + * @return A new {@code Expr} representing the dot product between the two vectors. + */ +export function dotProduct( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; + +/** + * @beta + * + * Calculates the dot product between a field's vector value and a vector expression. + * + * ```typescript + * // Calculate the dot product distance between two document vectors: 'docVector1' and 'docVector2' + * dotProduct("docVector1", field("docVector2")); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vectorExpression The other vector (represented as an Expr) to calculate with. + * @return A new {@code Expr} representing the dot product between the two vectors. + */ +export function dotProduct( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; + +/** + * @beta + * + * Calculates the dot product between a vector expression and a double array. + * + * ```typescript + * // Calculate the dot product between a feature vector and a target vector + * dotProduct(field("features"), [0.5, 0.8, 0.2]); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to calculate with. + * @param vector The other vector (as an array of doubles or VectorValue) to calculate with. + * @return A new {@code Expr} representing the dot product between the two vectors. + */ +export function dotProduct( + vectorExpression: Expr, + vector: number[] | VectorValue +): FunctionExpr; + +/** + * @beta + * + * Calculates the dot product between two vector expressions. + * + * ```typescript + * // Calculate the dot product between two document vectors: 'docVector1' and 'docVector2' + * dotProduct(field("docVector1"), field("docVector2")); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to calculate with. + * @param otherVectorExpression The other vector (represented as an Expr) to calculate with. + * @return A new {@code Expr} representing the dot product between the two vectors. + */ +export function dotProduct( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function dotProduct( + expr: Expr | string, + other: Expr | number[] | VectorValue +): FunctionExpr { + const expr1 = fieldOrExpression(expr); + const expr2 = vectorToExpr(other); + return expr1.dotProduct(expr2); +} + +/** + * @beta + * + * Calculates the Euclidean distance between a field's vector value and a double array. + * + * ```typescript + * // Calculate the Euclidean distance between the 'location' field and a target location + * euclideanDistance("location", [37.7749, -122.4194]); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vector The other vector (as an array of doubles or VectorValue) to compare against. + * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + */ +export function euclideanDistance( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; + +/** + * @beta + * + * Calculates the Euclidean distance between a field's vector value and a vector expression. + * + * ```typescript + * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' + * euclideanDistance("pointA", field("pointB")); + * ``` + * + * @param fieldName The name of the field containing the first vector. + * @param vectorExpression The other vector (represented as an Expr) to compare against. + * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + */ +export function euclideanDistance( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; + +/** + * @beta + * + * Calculates the Euclidean distance between a vector expression and a double array. + * + * ```typescript + * // Calculate the Euclidean distance between the 'location' field and a target location + * + * euclideanDistance(field("location"), [37.7749, -122.4194]); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to compare against. + * @param vector The other vector (as an array of doubles or VectorValue) to compare against. + * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + */ +export function euclideanDistance( + vectorExpression: Expr, + vector: number[] | VectorValue +): FunctionExpr; + +/** + * @beta + * + * Calculates the Euclidean distance between two vector expressions. + * + * ```typescript + * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' + * euclideanDistance(field("pointA"), field("pointB")); + * ``` + * + * @param vectorExpression The first vector (represented as an Expr) to compare against. + * @param otherVectorExpression The other vector (represented as an Expr) to compare against. + * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + */ +export function euclideanDistance( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function euclideanDistance( + expr: Expr | string, + other: Expr | number[] | VectorValue +): FunctionExpr { + const expr1 = fieldOrExpression(expr); + const expr2 = vectorToExpr(other); + return expr1.euclideanDistance(expr2); +} + +/** + * @beta + * + * Creates an expression that calculates the length of a Firestore Vector. + * + * ```typescript + * // Get the vector length (dimension) of the field 'embedding'. + * vectorLength(field("embedding")); + * ``` + * + * @param vectorExpression The expression representing the Firestore Vector. + * @return A new {@code Expr} representing the length of the array. + */ +export function vectorLength(vectorExpression: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that calculates the length of a Firestore Vector represented by a field. + * + * ```typescript + * // Get the vector length (dimension) of the field 'embedding'. + * vectorLength("embedding"); + * ``` + * + * @param fieldName The name of the field representing the Firestore Vector. + * @return A new {@code Expr} representing the length of the array. + */ +export function vectorLength(fieldName: string): FunctionExpr; +export function vectorLength(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).vectorLength(); +} + +/** + * @beta + * + * Creates an expression that interprets an expression as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'microseconds' field as microseconds since epoch. + * unixMicrosToTimestamp(field("microseconds")); + * ``` + * + * @param expr The expression representing the number of microseconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that interprets a field's value as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'microseconds' field as microseconds since epoch. + * unixMicrosToTimestamp("microseconds"); + * ``` + * + * @param fieldName The name of the field representing the number of microseconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; +export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).unixMicrosToTimestamp(); +} + +/** + * @beta + * + * Creates an expression that converts a timestamp expression to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to microseconds since epoch. + * timestampToUnixMicros(field("timestamp")); + * ``` + * + * @param expr The expression representing the timestamp. + * @return A new {@code Expr} representing the number of microseconds since epoch. + */ +export function timestampToUnixMicros(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that converts a timestamp field to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to microseconds since epoch. + * timestampToUnixMicros("timestamp"); + * ``` + * + * @param fieldName The name of the field representing the timestamp. + * @return A new {@code Expr} representing the number of microseconds since epoch. + */ +export function timestampToUnixMicros(fieldName: string): FunctionExpr; +export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { + return fieldOrExpression(expr).timestampToUnixMicros(); +} + +/** + * @beta + * + * Creates an expression that interprets an expression as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'milliseconds' field as milliseconds since epoch. + * unixMillisToTimestamp(field("milliseconds")); + * ``` + * + * @param expr The expression representing the number of milliseconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixMillisToTimestamp(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that interprets a field's value as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'milliseconds' field as milliseconds since epoch. + * unixMillisToTimestamp("milliseconds"); + * ``` + * + * @param fieldName The name of the field representing the number of milliseconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixMillisToTimestamp(fieldName: string): FunctionExpr; +export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.unixMillisToTimestamp(); +} + +/** + * @beta + * + * Creates an expression that converts a timestamp expression to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to milliseconds since epoch. + * timestampToUnixMillis(field("timestamp")); + * ``` + * + * @param expr The expression representing the timestamp. + * @return A new {@code Expr} representing the number of milliseconds since epoch. + */ +export function timestampToUnixMillis(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that converts a timestamp field to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to milliseconds since epoch. + * timestampToUnixMillis("timestamp"); + * ``` + * + * @param fieldName The name of the field representing the timestamp. + * @return A new {@code Expr} representing the number of milliseconds since epoch. + */ +export function timestampToUnixMillis(fieldName: string): FunctionExpr; +export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.timestampToUnixMillis(); +} + +/** + * @beta + * + * Creates an expression that interprets an expression as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'seconds' field as seconds since epoch. + * unixSecondsToTimestamp(field("seconds")); + * ``` + * + * @param expr The expression representing the number of seconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that interprets a field's value as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) + * and returns a timestamp. + * + * ```typescript + * // Interpret the 'seconds' field as seconds since epoch. + * unixSecondsToTimestamp("seconds"); + * ``` + * + * @param fieldName The name of the field representing the number of seconds since epoch. + * @return A new {@code Expr} representing the timestamp. + */ +export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; +export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.unixSecondsToTimestamp(); +} + +/** + * @beta + * + * Creates an expression that converts a timestamp expression to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to seconds since epoch. + * timestampToUnixSeconds(field("timestamp")); + * ``` + * + * @param expr The expression representing the timestamp. + * @return A new {@code Expr} representing the number of seconds since epoch. + */ +export function timestampToUnixSeconds(expr: Expr): FunctionExpr; + +/** + * @beta + * + * Creates an expression that converts a timestamp field to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * ```typescript + * // Convert the 'timestamp' field to seconds since epoch. + * timestampToUnixSeconds("timestamp"); + * ``` + * + * @param fieldName The name of the field representing the timestamp. + * @return A new {@code Expr} representing the number of seconds since epoch. + */ +export function timestampToUnixSeconds(fieldName: string): FunctionExpr; +export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.timestampToUnixSeconds(); +} + +/** + * @beta + * + * Creates an expression that adds a specified amount of time to a timestamp. + * + * ```typescript + * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. + * timestampAdd(field("timestamp"), field("unit"), field("amount")); + * ``` + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. + * @param amount The expression evaluates to amount of the unit. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampAdd( + timestamp: Expr, + unit: Expr, + amount: Expr +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that adds a specified amount of time to a timestamp. + * + * ```typescript + * // Add 1 day to the 'timestamp' field. + * timestampAdd(field("timestamp"), "day", 1); + * ``` + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to add (e.g., "day", "hour"). + * @param amount The amount of time to add. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampAdd( + timestamp: Expr, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that adds a specified amount of time to a timestamp represented by a field. + * + * ```typescript + * // Add 1 day to the 'timestamp' field. + * timestampAdd("timestamp", "day", 1); + * ``` + * + * @param fieldName The name of the field representing the timestamp. + * @param unit The unit of time to add (e.g., "day", "hour"). + * @param amount The amount of time to add. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampAdd( + fieldName: string, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpr; +export function timestampAdd( + timestamp: Expr | string, + unit: + | Expr + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expr | number +): FunctionExpr { + const normalizedTimestamp = fieldOrExpression(timestamp); + const normalizedUnit = valueToDefaultExpr(unit); + const normalizedAmount = valueToDefaultExpr(amount); + return normalizedTimestamp.timestampAdd(normalizedUnit, normalizedAmount); +} + +/** + * @beta + * + * Creates an expression that subtracts a specified amount of time from a timestamp. + * + * ```typescript + * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. + * timestampSub(field("timestamp"), field("unit"), field("amount")); + * ``` + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. + * @param amount The expression evaluates to amount of the unit. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampSub( + timestamp: Expr, + unit: Expr, + amount: Expr +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that subtracts a specified amount of time from a timestamp. + * + * ```typescript + * // Subtract 1 day from the 'timestamp' field. + * timestampSub(field("timestamp"), "day", 1); + * ``` + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to subtract (e.g., "day", "hour"). + * @param amount The amount of time to subtract. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampSub( + timestamp: Expr, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpr; + +/** + * @beta + * + * Creates an expression that subtracts a specified amount of time from a timestamp represented by a field. + * + * ```typescript + * // Subtract 1 day from the 'timestamp' field. + * timestampSub("timestamp", "day", 1); + * ``` + * + * @param fieldName The name of the field representing the timestamp. + * @param unit The unit of time to subtract (e.g., "day", "hour"). + * @param amount The amount of time to subtract. + * @return A new {@code Expr} representing the resulting timestamp. + */ +export function timestampSub( + fieldName: string, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpr; +export function timestampSub( + timestamp: Expr | string, + unit: + | Expr + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expr | number +): FunctionExpr { + const normalizedTimestamp = fieldOrExpression(timestamp); + const normalizedUnit = valueToDefaultExpr(unit); + const normalizedAmount = valueToDefaultExpr(amount); + return normalizedTimestamp.timestampSub(normalizedUnit, normalizedAmount); +} + +/** + * @beta + * + * Creates an expression that performs a logical 'AND' operation on multiple filter conditions. + * + * ```typescript + * // Check if the 'age' field is greater than 18 AND the 'city' field is "London" AND + * // the 'status' field is "active" + * const condition = and(gt("age", 18), eq("city", "London"), eq("status", "active")); + * ``` + * + * @param first The first filter condition. + * @param second The second filter condition. + * @param more Additional filter conditions to 'AND' together. + * @return A new {@code Expr} representing the logical 'AND' operation. + */ +export function and( + first: BooleanExpr, + second: BooleanExpr, + ...more: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('and', [first, second, ...more]); +} + +/** + * @beta + * + * Creates an expression that performs a logical 'OR' operation on multiple filter conditions. + * + * ```typescript + * // Check if the 'age' field is greater than 18 OR the 'city' field is "London" OR + * // the 'status' field is "active" + * const condition = or(gt("age", 18), eq("city", "London"), eq("status", "active")); + * ``` + * + * @param first The first filter condition. + * @param second The second filter condition. + * @param more Additional filter conditions to 'OR' together. + * @return A new {@code Expr} representing the logical 'OR' operation. + */ +export function or( + first: BooleanExpr, + second: BooleanExpr, + ...more: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('or', [first, second, ...more]); +} + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in ascending order based on an expression. + * + * ```typescript + * // Sort documents by the 'name' field in lowercase in ascending order + * firestore.pipeline().collection("users") + * .sort(ascending(field("name").toLower())); + * ``` + * + * @param expr The expression to create an ascending ordering for. + * @return A new `Ordering` for ascending sorting. + */ +export function ascending(expr: Expr): Ordering; + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in ascending order based on a field. + * + * ```typescript + * // Sort documents by the 'name' field in ascending order + * firestore.pipeline().collection("users") + * .sort(ascending("name")); + * ``` + * + * @param fieldName The field to create an ascending ordering for. + * @return A new `Ordering` for ascending sorting. + */ +export function ascending(fieldName: string): Ordering; +export function ascending(field: Expr | string): Ordering { + return new Ordering(fieldOrExpression(field), 'ascending'); +} + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in descending order based on an expression. + * + * ```typescript + * // Sort documents by the 'name' field in lowercase in descending order + * firestore.pipeline().collection("users") + * .sort(descending(field("name").toLower())); + * ``` + * + * @param expr The expression to create a descending ordering for. + * @return A new `Ordering` for descending sorting. + */ +export function descending(expr: Expr): Ordering; + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in descending order based on a field. + * + * ```typescript + * // Sort documents by the 'name' field in descending order + * firestore.pipeline().collection("users") + * .sort(descending("name")); + * ``` + * + * @param fieldName The field to create a descending ordering for. + * @return A new `Ordering` for descending sorting. + */ +export function descending(fieldName: string): Ordering; +export function descending(field: Expr | string): Ordering { + return new Ordering(fieldOrExpression(field), 'descending'); +} + +/** + * @beta + * + * Represents an ordering criterion for sorting documents in a Firestore pipeline. + * + * You create `Ordering` instances using the `ascending` and `descending` helper functions. + */ +export class Ordering implements ProtoValueSerializable, UserData { + constructor( + readonly expr: Expr, + readonly direction: 'ascending' | 'descending' + ) {} + + /** + * @internal + * @private + * Indicates if this expression was created from a literal value passed + * by the caller. + */ + _createdFromLiteral: boolean = false; + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + mapValue: { + fields: { + direction: toStringValue(this.direction), + expression: this.expr._toProto(serializer) + } + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + context = + this._createdFromLiteral && context + ? context + : dataReader.createContext(UserDataSource.Argument, 'constant'); + this.expr._readUserData(dataReader); + } + + _protoValueType: 'ProtoValue' = 'ProtoValue'; +} diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts new file mode 100644 index 00000000000..635636ac46b --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -0,0 +1,240 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ObjectValue } from '../model/object_value'; +import { isOptionalEqual } from '../util/misc'; + +import { Field } from './expressions'; +import { FieldPath } from './field_path'; +import { Pipeline } from './pipeline'; +import { DocumentData, DocumentReference, refEqual } from './reference'; +import { fieldPathFromArgument } from './snapshot'; +import { Timestamp } from './timestamp'; +import { AbstractUserDataWriter } from './user_data_writer'; + +export class PipelineSnapshot { + private readonly _pipeline: Pipeline; + private readonly _executionTime: Timestamp | undefined; + private readonly _results: PipelineResult[]; + constructor( + pipeline: Pipeline, + results: PipelineResult[], + executionTime?: Timestamp + ) { + this._pipeline = pipeline; + this._executionTime = executionTime; + this._results = results; + } + + /** + * The Pipeline on which you called `execute()` in order to get this + * `PipelineSnapshot`. + */ + get pipeline(): Pipeline { + return this._pipeline; + } + + /** An array of all the results in the `PipelineSnapshot`. */ + get results(): PipelineResult[] { + return this._results; + } + + /** + * The time at which the pipeline producing this result is executed. + * + * @type {Timestamp} + * @readonly + * + */ + get executionTime(): Timestamp { + if (this._executionTime === undefined) { + throw new Error( + "'executionTime' is expected to exist, but it is undefined" + ); + } + return this._executionTime; + } +} + +/** + * @beta + * + * A PipelineResult contains data read from a Firestore Pipeline. The data can be extracted with the + * {@link #data()} or {@link #get(String)} methods. + * + *

If the PipelineResult represents a non-document result, `ref` will return a undefined + * value. + */ +export class PipelineResult { + private readonly _userDataWriter: AbstractUserDataWriter; + + private readonly _createTime: Timestamp | undefined; + private readonly _updateTime: Timestamp | undefined; + + /** + * @internal + * @private + */ + readonly _ref: DocumentReference | undefined; + + /** + * @internal + * @private + */ + readonly _fields: ObjectValue | undefined; + + /** + * @private + * @internal + * + * @param userDataWriter The serializer used to encode/decode protobuf. + * @param ref The reference to the document. + * @param fields The fields of the Firestore `Document` Protobuf backing + * this document (or undefined if the document does not exist). + * @param readTime The time when this result was read (or undefined if + * the document exists only locally). + * @param createTime The time when the document was created if the result is a document, undefined otherwise. + * @param updateTime The time when the document was last updated if the result is a document, undefined otherwise. + */ + constructor( + userDataWriter: AbstractUserDataWriter, + ref?: DocumentReference, + fields?: ObjectValue, + createTime?: Timestamp, + updateTime?: Timestamp + ) { + this._ref = ref; + this._userDataWriter = userDataWriter; + this._createTime = createTime; + this._updateTime = updateTime; + this._fields = fields; + } + + /** + * The reference of the document, if it is a document; otherwise `undefined`. + */ + get ref(): DocumentReference | undefined { + return this._ref; + } + + /** + * The ID of the document for which this PipelineResult contains data, if it is a document; otherwise `undefined`. + * + * @type {string} + * @readonly + * + */ + get id(): string | undefined { + return this._ref?.id; + } + + /** + * The time the document was created. Undefined if this result is not a document. + * + * @type {Timestamp|undefined} + * @readonly + */ + get createTime(): Timestamp | undefined { + return this._createTime; + } + + /** + * The time the document was last updated (at the time the snapshot was + * generated). Undefined if this result is not a document. + * + * @type {Timestamp|undefined} + * @readonly + */ + get updateTime(): Timestamp | undefined { + return this._updateTime; + } + + /** + * Retrieves all fields in the result as an object. Returns 'undefined' if + * the document doesn't exist. + * + * @returns {T|undefined} An object containing all fields in the document or + * 'undefined' if the document doesn't exist. + * + * @example + * ``` + * let p = firestore.pipeline().collection('col'); + * + * p.execute().then(results => { + * let data = results[0].data(); + * console.log(`Retrieved data: ${JSON.stringify(data)}`); + * }); + * ``` + */ + data(): AppModelType | undefined { + if (this._fields === undefined) { + return undefined; + } + + return this._userDataWriter.convertValue( + this._fields.value + ) as AppModelType; + } + + /** + * Retrieves the field specified by `field`. + * + * @param {string|FieldPath|Field} field The field path + * (e.g. 'foo' or 'foo.bar') to a specific field. + * @returns {*} The data at the specified field location or undefined if no + * such field exists. + * + * @example + * ``` + * let p = firestore.pipeline().collection('col'); + * + * p.execute().then(results => { + * let field = results[0].get('a.b'); + * console.log(`Retrieved field value: ${field}`); + * }); + * ``` + */ + // We deliberately use `any` in the external API to not impose type-checking + // on end users. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get(fieldPath: string | FieldPath | Field): any { + if (this._fields === undefined) { + return undefined; + } + + const value = this._fields.field( + fieldPathFromArgument('DocumentSnapshot.get', fieldPath) + ); + if (value !== null) { + return this._userDataWriter.convertValue(value); + } + } +} + +export function pipelineResultEqual( + left: PipelineResult, + right: PipelineResult +): boolean { + if (left === right) { + return true; + } + + return ( + isOptionalEqual(left._ref, right._ref, refEqual) && + isOptionalEqual(left._fields, right._fields, (l, r) => l.isEqual(r)) + ); +} diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts new file mode 100644 index 00000000000..421fc759bfb --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DatabaseId } from '../core/database_info'; +import { toPipeline } from '../core/pipeline-util'; +import { FirestoreError, Code } from '../util/error'; + +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference, Query } from './reference'; +import { + CollectionGroupSource, + CollectionSource, + DatabaseSource, + DocumentsSource, + Stage +} from './stage'; + +/** + * Represents the source of a Firestore {@link Pipeline}. + * @beta + */ +export class PipelineSource { + /** + * @internal + * @private + * @param _createPipeline + */ + constructor( + private databaseId: DatabaseId, + /** + * @internal + * @private + */ + public _createPipeline: (stages: Stage[]) => PipelineType + ) {} + + /** + * Set the pipeline's source to the collection specified by the given path. + * + * @param collectionPath A path to a collection that will be the source of this pipeline. + */ + collection(collectionPath: string): PipelineType; + + /** + * Set the pipeline's source to the collection specified by the given CollectionReference. + * + * @param collectionReference A CollectionReference for a collection that will be the source of this pipeline. + * The converter for this CollectionReference will be ignored and not have an effect on this pipeline. + * + * @throws {@FirestoreError} Thrown if the provided CollectionReference targets a different project or database than the pipeline. + */ + collection(collectionReference: CollectionReference): PipelineType; + collection(collection: CollectionReference | string): PipelineType { + if (collection instanceof CollectionReference) { + this._validateReference(collection); + return this._createPipeline([new CollectionSource(collection.path)]); + } else { + return this._createPipeline([new CollectionSource(collection)]); + } + } + + /** + * Set the pipeline's source to the collection group with the given id. + * + * @param collectionid The id of a collection group that will be the source of this pipeline. + */ + collectionGroup(collectionId: string): PipelineType { + return this._createPipeline([new CollectionGroupSource(collectionId)]); + } + + /** + * Set the pipeline's source to be all documents in this database. + */ + database(): PipelineType { + return this._createPipeline([new DatabaseSource()]); + } + + /** + * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. + * + * @param docs An array of paths and DocumentReferences specifying the individual documents that will be the source of this pipeline. + * The converters for these DocumentReferences will be ignored and not have an effect on this pipeline. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + documents(docs: Array): PipelineType { + docs.forEach(doc => { + if (doc instanceof DocumentReference) { + this._validateReference(doc); + } + }); + + return this._createPipeline([DocumentsSource.of(docs)]); + } + + /** + * Convert the given Query into an equivalent Pipeline. + * + * @param query A Query to be converted into a Pipeline. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + createFrom(query: Query): Pipeline { + return toPipeline(query._query, query.firestore); + } + + _validateReference(reference: CollectionReference | DocumentReference): void { + const refDbId = reference.firestore._databaseId; + if (!refDbId.isEqual(this.databaseId)) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Invalid ${ + reference instanceof CollectionReference + ? 'CollectionReference' + : 'DocumentReference' + }. ` + + `The project ID ("${refDbId.projectId}") or the database ("${refDbId.database}") does not match ` + + `the project ID ("${this.databaseId.projectId}") and database ("${this.databaseId.database}") of the target database of this Pipeline.` + ); + } + } +} diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts new file mode 100644 index 00000000000..8b5bebeddb8 --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -0,0 +1,872 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ObjectValue } from '../model/object_value'; +import { + Pipeline as ProtoPipeline, + Stage as ProtoStage +} from '../protos/firestore_proto_api'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; +import { isPlainObject } from '../util/input_validation'; + +import { Firestore } from './database'; +import { + _mapValue, + AggregateFunction, + AggregateWithAlias, + Expr, + ExprWithAlias, + Field, + BooleanExpr, + Ordering, + Selectable, + field, + Constant +} from './expressions'; +import { + AddFields, + Aggregate, + Distinct, + FindNearest, + FindNearestOptions, + GenericStage, + Limit, + Offset, + RemoveFields, + Replace, + Select, + Sort, + Sample, + Union, + Unnest, + Stage, + Where +} from './stage'; +import { + parseVectorValue, + UserDataReader, + UserDataSource +} from './user_data_reader'; +import { AbstractUserDataWriter } from './user_data_writer'; + +interface ReadableUserData { + _readUserData(dataReader: UserDataReader): void; +} + +function isReadableUserData(value: unknown): value is ReadableUserData { + return typeof (value as ReadableUserData)._readUserData === 'function'; +} + +/** + * @beta + * + * The Pipeline class provides a flexible and expressive framework for building complex data + * transformation and query pipelines for Firestore. + * + * A pipeline takes data sources, such as Firestore collections or collection groups, and applies + * a series of stages that are chained together. Each stage takes the output from the previous stage + * (or the data source) and produces an output for the next stage (or as the final output of the + * pipeline). + * + * Expressions can be used within each stage to filter and transform data through the stage. + * + * NOTE: The chained stages do not prescribe exactly how Firestore will execute the pipeline. + * Instead, Firestore only guarantees that the result is the same as if the chained stages were + * executed in order. + * + * Usage Examples: + * + * ```typescript + * const db: Firestore; // Assumes a valid firestore instance. + * + * // Example 1: Select specific fields and rename 'rating' to 'bookRating' + * const results1 = await execute(db.pipeline() + * .collection("books") + * .select("title", "author", field("rating").as("bookRating"))); + * + * // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950 + * const results2 = await execute(db.pipeline() + * .collection("books") + * .where(and(field("genre").eq("Science Fiction"), field("published").gt(1950)))); + * + * // Example 3: Calculate the average rating of books published after 1980 + * const results3 = await execute(db.pipeline() + * .collection("books") + * .where(field("published").gt(1980)) + * .aggregate(avg(field("rating")).as("averageRating"))); + * ``` + */ + +/** + * Base-class implementation + */ +export class Pipeline implements ProtoSerializable { + /** + * @internal + * @private + * @param _db + * @param userDataReader + * @param _userDataWriter + * @param stages + */ + constructor( + /** + * @internal + * @private + */ + public _db: Firestore, + private userDataReader: UserDataReader, + /** + * @internal + * @private + */ + public _userDataWriter: AbstractUserDataWriter, + private stages: Stage[] + ) {} + + /** + * Adds new fields to outputs from previous stages. + * + * This stage allows you to compute values on-the-fly based on existing data from previous + * stages or constants. You can use this to create new fields or overwrite existing ones (if there + * is name overlaps). + * + * The added fields are defined using {@link Selectable}s, which can be: + * + * - {@link Field}: References an existing document field. + * - {@link Expr}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expr#as}. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .addFields( + * field("rating").as("bookRating"), // Rename 'rating' to 'bookRating' + * add(5, field("quantity")).as("totalCost") // Calculate 'totalCost' + * ); + * ``` + * + * @param field The first field to add to the documents, specified as a {@link Selectable}. + * @param additionalFields Optional additional fields to add to the documents, specified as {@link Selectable}s. + * @return A new Pipeline object with this stage appended to the stage list. + */ + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline { + return this._addStage( + new AddFields( + this.readUserData( + 'addFields', + this.selectablesToMap([field, ...additionalFields]) + ) + ) + ); + } + + /** + * Remove fields from outputs of previous stages. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection('books') + * // removes field 'rating' and 'cost' from the previous stage outputs. + * .removeFields( + * field('rating'), + * 'cost' + * ); + * ``` + * + * @param fieldValue The first field to remove. + * @param additionalFields Optional additional fields to remove. + * @return A new Pipeline object with this stage appended to the stage list. + */ + removeFields( + fieldValue: Field | string, + ...additionalFields: Array + ): Pipeline { + const fieldExpressions = [fieldValue, ...additionalFields].map(f => + typeof f === 'string' ? field(f) : (f as Field) + ); + this.readUserData('removeFields', fieldExpressions); + return this._addStage(new RemoveFields(fieldExpressions)); + } + + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + *

The selected fields are defined using {@link Selectable} expressions, which can be: + * + *

    + *
  • {@code string}: Name of an existing field
  • + *
  • {@link Field}: References an existing field.
  • + *
  • {@link Function}: Represents the result of a function with an assigned alias name using + * {@link Expr#as}
  • + *
+ * + *

If no selections are provided, the output of this stage is empty. Use {@link + * Pipeline#addFields} instead if only additions are + * desired. + * + *

Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .select( + * "firstName", + * field("lastName"), + * field("address").toUppercase().as("upperAddress"), + * ); + * ``` + * + * @param selection The first field to include in the output documents, specified as {@link + * Selectable} expression or string value representing the field name. + * @param additionalSelections Optional additional fields to include in the output documents, specified as {@link + * Selectable} expressions or {@code string} values representing field names. + * @return A new Pipeline object with this stage appended to the stage list. + */ + select( + selection: Selectable | string, + ...additionalSelections: Array + ): Pipeline { + let projections: Map = this.selectablesToMap([ + selection, + ...additionalSelections + ]); + projections = this.readUserData('select', projections); + return this._addStage(new Select(projections)); + } + + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpr}. + * + *

This stage allows you to apply conditions to the data, similar to a "WHERE" clause in SQL. + * You can filter documents based on their field values, using implementations of {@link + * BooleanExpr}, typically including but not limited to: + * + *

    + *
  • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link + * Function#gt} (greater than), etc.
  • + *
  • logical operators: {@link Function#and}, {@link Function#or}, {@link Function#not}, etc.
  • + *
  • advanced functions: {@link Function#regexMatch}, {@link + * Function#arrayContains}, etc.
  • + *
+ * + *

Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .where( + * and( + * gt(field("rating"), 4.0), // Filter for ratings greater than 4.0 + * field("genre").eq("Science Fiction") // Equivalent to gt("genre", "Science Fiction") + * ) + * ); + * ``` + * + * @param condition The {@link BooleanExpr} to apply. + * @return A new Pipeline object with this stage appended to the stage list. + */ + where(condition: BooleanExpr): Pipeline { + this.readUserData('where', condition); + return this._addStage(new Where(condition)); + } + + /** + * Skips the first `offset` number of documents from the results of previous stages. + * + *

This stage is useful for implementing pagination in your pipelines, allowing you to retrieve + * results in chunks. It is typically used in conjunction with {@link #limit} to control the + * size of each page. + * + *

Example: + * + * ```typescript + * // Retrieve the second page of 20 results + * firestore.pipeline().collection("books") + * .sort(field("published").descending()) + * .offset(20) // Skip the first 20 results + * .limit(20); // Take the next 20 results + * ``` + * + * @param offset The number of documents to skip. + * @return A new Pipeline object with this stage appended to the stage list. + */ + offset(offset: number): Pipeline { + return this._addStage(new Offset(offset)); + } + + /** + * Limits the maximum number of documents returned by previous stages to `limit`. + * + *

This stage is particularly useful when you want to retrieve a controlled subset of data from + * a potentially large result set. It's often used for: + * + *

    + *
  • **Pagination:** In combination with {@link #offset} to retrieve specific pages of + * results.
  • + *
  • **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, + * especially when dealing with large collections.
  • + *
+ * + *

Example: + * + * ```typescript + * // Limit the results to the top 10 highest-rated books + * firestore.pipeline().collection("books") + * .sort(field("rating").descending()) + * .limit(10); + * ``` + * + * @param limit The maximum number of documents to return. + * @return A new Pipeline object with this stage appended to the stage list. + */ + limit(limit: number): Pipeline { + return this._addStage(new Limit(limit)); + } + + /** + * Returns a set of distinct values from the inputs to this stage. + * + * This stage runs through the results from previous stages to include only results with + * unique combinations of {@link Expr} values ({@link Field}, {@link Function}, etc). + * + * The parameters to this stage are defined using {@link Selectable} expressions or strings: + * + * - {@code string}: Name of an existing field + * - {@link Field}: References an existing document field. + * - {@link ExprWithAlias}: Represents the result of a function with an assigned alias name + * using {@link Expr#as}. + * + * Example: + * + * ```typescript + * // Get a list of unique author names in uppercase and genre combinations. + * firestore.pipeline().collection("books") + * .distinct(toUppercase(field("author")).as("authorName"), field("genre"), "publishedAt") + * .select("authorName"); + * ``` + * + * @param group The {@link Selectable} expression or field name to consider when determining + * distinct value combinations. + * @param additionalGroups Optional additional {@link Selectable} expressions to consider when determining distinct + * value combinations or strings representing field names. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + distinct( + group: string | Selectable, + ...additionalGroups: Array + ): Pipeline { + return this._addStage( + new Distinct( + this.readUserData( + 'distinct', + this.selectablesToMap([group, ...additionalGroups]) + ) + ) + ); + } + + /** + * Performs aggregation operations on the documents from previous stages. + * + *

This stage allows you to calculate aggregate values over a set of documents. You define the + * aggregations to perform using {@link AggregateWithAlias} expressions which are typically results of + * calling {@link Expr#as} on {@link AggregateFunction} instances. + * + *

Example: + * + * ```typescript + * // Calculate the average rating and the total number of books + * firestore.pipeline().collection("books") + * .aggregate( + * field("rating").avg().as("averageRating"), + * countAll().as("totalBooks") + * ); + * ``` + * + * @param accumulator The first {@link AggregateWithAlias}, wrapping an {@link AggregateFunction} + * and providing a name for the accumulated results. + * @param additionalAccumulators Optional additional {@link AggregateWithAlias}, each wrapping an {@link AggregateFunction} + * and providing a name for the accumulated results. + * @return A new Pipeline object with this stage appended to the stage list. + */ + aggregate( + accumulator: AggregateWithAlias, + ...additionalAccumulators: AggregateWithAlias[] + ): Pipeline; + /** + * Performs optionally grouped aggregation operations on the documents from previous stages. + * + *

This stage allows you to calculate aggregate values over a set of documents, optionally + * grouped by one or more fields or functions. You can specify: + * + *

    + *
  • **Grouping Fields or Functions:** One or more fields or functions to group the documents + * by. For each distinct combination of values in these fields, a separate group is created. + * If no grouping fields are provided, a single group containing all documents is used. Not + * specifying groups is the same as putting the entire inputs into one group.
  • + *
  • **Accumulators:** One or more accumulation operations to perform within each group. These + * are defined using {@link AggregateWithAlias} expressions, which are typically created by + * calling {@link Expr#as} on {@link AggregateFunction} instances. Each aggregation + * calculates a value (e.g., sum, average, count) based on the documents within its group.
  • + *
+ * + *

Example: + * + * ```typescript + * // Calculate the average rating for each genre. + * firestore.pipeline().collection("books") + * .aggregate({ + * accumulators: [avg(field("rating")).as("avg_rating")] + * groups: ["genre"] + * }); + * ``` + * + * @param options An object that specifies the accumulators + * and optional grouping fields to perform. + * @return A new {@code Pipeline} object with this stage appended to the stage + * list. + */ + aggregate(options: { + accumulators: AggregateWithAlias[]; + groups?: Array; + }): Pipeline; + aggregate( + optionsOrTarget: + | AggregateWithAlias + | { + accumulators: AggregateWithAlias[]; + groups?: Array; + }, + ...rest: AggregateWithAlias[] + ): Pipeline { + if ('accumulators' in optionsOrTarget) { + return this._addStage( + new Aggregate( + new Map( + optionsOrTarget.accumulators.map((target: AggregateWithAlias) => { + this.readUserData( + 'aggregate', + target as unknown as AggregateWithAlias + ); + return [ + (target as unknown as AggregateWithAlias).alias, + (target as unknown as AggregateWithAlias).aggregate + ]; + }) + ), + this.readUserData( + 'aggregate', + this.selectablesToMap(optionsOrTarget.groups || []) + ) + ) + ); + } else { + return this._addStage( + new Aggregate( + new Map( + [optionsOrTarget, ...rest].map(target => [ + (target as unknown as AggregateWithAlias).alias, + this.readUserData( + 'aggregate', + (target as unknown as AggregateWithAlias).aggregate + ) + ]) + ), + new Map() + ) + ); + } + } + + findNearest(options: FindNearestOptions): Pipeline { + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'findNearest' + ); + const value = parseVectorValue(options.vectorValue, parseContext); + const vectorObjectValue = new ObjectValue(value); + return this._addStage( + new FindNearest( + options.field instanceof Field ? options.field : field(options.field), + vectorObjectValue, + options.distanceMeasure, + options.limit, + options.distanceField + ) + ); + } + + /** + * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. + * + *

This stage allows you to order the results of your pipeline. You can specify multiple {@link + * Ordering} instances to sort by multiple fields in ascending or descending order. If documents + * have the same value for a field used for sorting, the next specified ordering will be used. If + * all orderings result in equal comparison, the documents are considered equal and the order is + * unspecified. + * + *

Example: + * + * ```typescript + * // Sort books by rating in descending order, and then by title in ascending order for books + * // with the same rating + * firestore.pipeline().collection("books") + * .sort( + * Ordering.of(field("rating")).descending(), + * Ordering.of(field("title")) // Ascending order is the default + * ); + * ``` + * + * @param ordering The first {@link Ordering} instance specifying the sorting criteria. + * @param additionalOrderings Optional additional {@link Ordering} instances specifying the additional sorting criteria. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline { + // Ordering object + return this._addStage( + new Sort(this.readUserData('sort', [ordering, ...additionalOrderings])) + ); + } + + /** + * Fully overwrites all fields in a document with those coming from a nested map. + * + *

This stage allows you to emit a map value as a document. Each key of the map becomes a field + * on the document that contains the corresponding value. + * + *

Example: + * + * ```typescript + * // Input. + * // { + * // 'name': 'John Doe Jr.', + * // 'parents': { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * // } + * + * // Emit parents as document. + * firestore.pipeline().collection('people').replaceWith('parents'); + * + * // Output + * // { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * ``` + * + * @param fieldName The {@link Field} field containing the nested map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(fieldName: string): Pipeline; + + /** + * Fully overwrites all fields in a document with those coming from a map. + * + *

This stage allows you to emit a map value as a document. Each key of the map becomes a field + * on the document that contains the corresponding value. + * + *

Example: + * + * ```typescript + * // Input. + * // { + * // 'name': 'John Doe Jr.', + * // 'parents': { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * // } + * + * // Emit parents as document. + * firestore.pipeline().collection('people').replaceWith(map({ + * foo: 'bar', + * info: { + * name: field('name') + * } + * })); + * + * // Output + * // { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * ``` + * + * @param expr An {@link Expr} that when returned evaluates to a map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(expr: Expr): Pipeline; + replaceWith(value: Expr | string): Pipeline { + const fieldExpr = typeof value === 'string' ? field(value) : value; + this.readUserData('replaceWith', fieldExpr); + return this._addStage(new Replace(fieldExpr, 'full_replace')); + } + + /** + * Performs a pseudo-random sampling of the documents from the previous stage. + * + *

This stage will filter documents pseudo-randomly. The parameter specifies how number of + * documents to be returned. + * + *

Examples: + * + * ```typescript + * // Sample 25 books, if available. + * firestore.pipeline().collection('books') + * .sample(25); + * ``` + * + * @param documents The number of documents to sample. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sample(documents: number): Pipeline; + + /** + * Performs a pseudo-random sampling of the documents from the previous stage. + * + *

This stage will filter documents pseudo-randomly. The 'options' parameter specifies how + * sampling will be performed. See {@code SampleOptions} for more information. + * + *

Examples: + * + * // Sample 10 books, if available. + * firestore.pipeline().collection("books") + * .sample({ documents: 10 }); + * + * // Sample 50% of books. + * firestore.pipeline().collection("books") + * .sample({ percentage: 0.5 }); + * + * @param options The {@code SampleOptions} specifies how sampling is performed. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sample(options: { percentage: number } | { documents: number }): Pipeline; + sample( + documentsOrOptions: number | { percentage: number } | { documents: number } + ): Pipeline { + if (typeof documentsOrOptions === 'number') { + return this._addStage(new Sample(documentsOrOptions, 'documents')); + } else if ('percentage' in documentsOrOptions) { + return this._addStage( + new Sample(documentsOrOptions.percentage, 'percent') + ); + } else { + return this._addStage( + new Sample(documentsOrOptions.documents, 'documents') + ); + } + } + + /** + * Performs union of all documents from two pipelines, including duplicates. + * + *

This stage will pass through documents from previous stage, and also pass through documents + * from previous stage of the `other` {@code Pipeline} given in parameter. The order of documents + * emitted from this stage is undefined. + * + *

Example: + * + * ```typescript + * // Emit documents from books collection and magazines collection. + * firestore.pipeline().collection('books') + * .union(firestore.pipeline().collection('magazines')); + * ``` + * + * @param other The other {@code Pipeline} that is part of union. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + union(other: Pipeline): Pipeline { + return this._addStage(new Union(other)); + } + + /** + * Produces a document for each element in an input array. + * + * For each previous stage document, this stage will emit zero or more augmented documents. The + * input array specified by the `selectable` parameter, will emit an augmented document for each input array element. The input array element will + * augment the previous stage document by setting the `alias` field with the array element value. + * + * When `selectable` evaluates to a non-array value (ex: number, null, absent), then the stage becomes a no-op for + * the current input document, returning it as is with the `alias` field absent. + * + * No documents are emitted when `selectable` evaluates to an empty array. + * + * Example: + * + * ```typescript + * // Input: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... } + * + * // Emit a book document for each tag of the book. + * firestore.pipeline().collection("books") + * .unnest(field("tags").as('tag'), 'tagIndex'); + * + * // Output: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", "tagIndex": 0, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", "tagIndex": 1, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", "tagIndex": 2, ... } + * ``` + * + * @param selectable A selectable expression defining the field to unnest and the alias to use for each un-nested element in the output documents. + * @param indexField An optional string value specifying the field path to write the offset (starting at zero) into the array the un-nested element is from + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + unnest(selectable: Selectable, indexField?: string): Pipeline { + this.readUserData('unnest', selectable.expr); + + const alias = field(selectable.alias); + this.readUserData('unnest', alias); + + if (indexField) { + return this._addStage( + new Unnest(selectable.expr, alias, field(indexField)) + ); + } else { + return this._addStage(new Unnest(selectable.expr, alias)); + } + } + + /** + * Adds a generic stage to the pipeline. + * + *

This method provides a flexible way to extend the pipeline's functionality by adding custom + * stages. Each generic stage is defined by a unique `name` and a set of `params` that control its + * behavior. + * + *

Example (Assuming there is no "where" stage available in SDK): + * + * ```typescript + * // Assume we don't have a built-in "where" stage + * firestore.pipeline().collection("books") + * .genericStage("where", [field("published").lt(1900)]) // Custom "where" stage + * .select("title", "author"); + * ``` + * + * @param name The unique name of the generic stage to add. + * @param params A list of parameters to configure the generic stage's behavior. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + genericStage(name: string, params: unknown[]): Pipeline { + // Convert input values to Expressions. + // We treat objects as mapValues and arrays as arrayValues, + // this is unlike the default conversion for objects and arrays + // passed to an expression. + const expressionParams = params.map((value: unknown) => { + if (value instanceof Expr) { + return value; + } else if (value instanceof AggregateFunction) { + return value; + } else if (isPlainObject(value)) { + return _mapValue(value as Record); + } else { + return new Constant(value); + } + }); + + expressionParams.forEach(param => { + if (isReadableUserData(param)) { + param._readUserData(this.userDataReader); + } + }); + return this._addStage(new GenericStage(name, expressionParams)); + } + + /** + * @internal + * @private + */ + _toProto(jsonProtoSerializer: JsonProtoSerializer): ProtoPipeline { + const stages: ProtoStage[] = this.stages.map(stage => + stage._toProto(jsonProtoSerializer) + ); + return { stages }; + } + + private _addStage(stage: Stage): Pipeline { + const copy = this.stages.map(s => s); + copy.push(stage); + return this.newPipeline( + this._db, + this.userDataReader, + this._userDataWriter, + copy + ); + } + + private selectablesToMap( + selectables: Array + ): Map { + const result = new Map(); + for (const selectable of selectables) { + if (typeof selectable === 'string') { + result.set(selectable as string, field(selectable)); + } else if (selectable instanceof Field) { + result.set(selectable.alias, selectable.expr); + } else if (selectable instanceof ExprWithAlias) { + result.set(selectable.alias, selectable.expr); + } + } + return result; + } + + /** + * Reads user data for each expression in the expressionMap. + * @param name Name of the calling function. Used for error messages when invalid user data is encountered. + * @param expressionMap + * @return the expressionMap argument. + * @private + */ + private readUserData< + T extends + | Map + | ReadableUserData[] + | ReadableUserData + >(name: string, expressionMap: T): T { + if (isReadableUserData(expressionMap)) { + expressionMap._readUserData(this.userDataReader); + } else if (Array.isArray(expressionMap)) { + expressionMap.forEach(readableData => + readableData._readUserData(this.userDataReader) + ); + } else { + expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); + } + return expressionMap; + } + + /** + * @internal + * @private + * @param db + * @param userDataReader + * @param userDataWriter + * @param stages + * @protected + */ + protected newPipeline( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + stages: Stage[] + ): Pipeline { + return new Pipeline(db, userDataReader, userDataWriter, stages); + } +} diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts new file mode 100644 index 00000000000..c1ca940a56b --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { invokeExecutePipeline } from '../remote/datastore'; + +import { getDatastore } from './components'; +import { Firestore } from './database'; +import { Pipeline } from './pipeline'; +import { PipelineResult, PipelineSnapshot } from './pipeline-result'; +import { PipelineSource } from './pipeline-source'; +import { DocumentReference } from './reference'; +import { LiteUserDataWriter } from './reference_impl'; +import { Stage } from './stage'; +import { newUserDataReader } from './user_data_reader'; + +declare module './database' { + interface Firestore { + pipeline(): PipelineSource; + } +} + +/** + * Executes this pipeline and returns a Promise to represent the asynchronous operation. + * + * The returned Promise can be used to track the progress of the pipeline execution + * and retrieve the results (or handle any errors) asynchronously. + * + * The pipeline results are returned as a {@link PipelineSnapshot} that contains + * a list of {@link PipelineResult} objects. Each {@link PipelineResult} typically + * represents a single key/value map that has passed through all the + * stages of the pipeline, however this might differ depending on the stages involved in the + * pipeline. For example: + * + *

    + *
  • If there are no stages or only transformation stages, each {@link PipelineResult} + * represents a single document.
  • + *
  • If there is an aggregation, only a single {@link PipelineResult} is returned, + * representing the aggregated results over the entire dataset .
  • + *
  • If there is an aggregation stage with grouping, each {@link PipelineResult} represents a + * distinct group and its associated aggregated values.
  • + *
+ * + *

Example: + * + * ```typescript + * const snapshot: PipelineSnapshot = await execute(firestore.pipeline().collection("books") + * .where(gt(field("rating"), 4.5)) + * .select("title", "author", "rating")); + * + * const results: PipelineResults = snapshot.results; + * ``` + * + * @param pipeline The pipeline to execute. + * @return A Promise representing the asynchronous pipeline execution. + */ +export function execute(pipeline: Pipeline): Promise { + const datastore = getDatastore(pipeline._db); + return invokeExecutePipeline(datastore, pipeline).then(result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; + + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.key?.path + ? new DocumentReference(pipeline._db, null, element.key) + : undefined, + element.fields, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) + ); + + return new PipelineSnapshot(pipeline, docs, executionTime); + }); +} + +Firestore.prototype.pipeline = function (): PipelineSource { + const userDataWriter = new LiteUserDataWriter(this); + const userDataReader = newUserDataReader(this); + return new PipelineSource(this._databaseId, (stages: Stage[]) => { + return new Pipeline(this, userDataReader, userDataWriter, stages); + }); +}; diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index f38dad9a078..e6c5fd7b056 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -651,7 +651,7 @@ export function doc( ) { throw new FirestoreError( Code.INVALID_ARGUMENT, - 'Expected first argument to collection() to be a CollectionReference, ' + + 'Expected first argument to doc() to be a CollectionReference, ' + 'a DocumentReference or FirebaseFirestore' ); } diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 3024e2e9db0..66c3a1422e9 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -23,6 +23,7 @@ import { FieldPath as InternalFieldPath } from '../model/path'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; +import { Field } from './expressions'; import { FieldPath } from './field_path'; import { DocumentData, @@ -515,12 +516,14 @@ export function snapshotEqual( */ export function fieldPathFromArgument( methodName: string, - arg: string | FieldPath | Compat + arg: string | FieldPath | Compat | Field ): InternalFieldPath { if (typeof arg === 'string') { return fieldPathFromDotSeparatedString(methodName, arg); } else if (arg instanceof FieldPath) { return arg._internalPath; + } else if (arg instanceof Field) { + return fieldPathFromDotSeparatedString(methodName, arg.fieldName()); } else { return arg._delegate._internalPath; } diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts new file mode 100644 index 00000000000..31faaa00e76 --- /dev/null +++ b/packages/firestore/src/lite-api/stage.ts @@ -0,0 +1,506 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ObjectValue } from '../model/object_value'; +import { + Stage as ProtoStage, + Value as ProtoValue +} from '../protos/firestore_proto_api'; +import { toNumber } from '../remote/number_serializer'; +import { + JsonProtoSerializer, + ProtoSerializable, + toMapValue, + toPipelineValue, + toStringValue +} from '../remote/serializer'; +import { hardAssert } from '../util/assert'; + +import { + AggregateFunction, + Expr, + Field, + BooleanExpr, + Ordering, + field +} from './expressions'; +import { Pipeline } from './pipeline'; +import { DocumentReference } from './reference'; +import { VectorValue } from './vector_value'; + +/** + * @beta + */ +export interface Stage extends ProtoSerializable { + name: string; +} + +/** + * @beta + */ +export class AddFields implements Stage { + name = 'add_fields'; + + constructor(private fields: Map) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toMapValue(serializer, this.fields)] + }; + } +} + +/** + * @beta + */ +export class RemoveFields implements Stage { + name = 'remove_fields'; + + constructor(private fields: Field[]) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.fields.map(f => f._toProto(serializer)) + }; + } +} + +/** + * @beta + */ +export class Aggregate implements Stage { + name = 'aggregate'; + + constructor( + private accumulators: Map, + private groups: Map + ) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [ + toMapValue(serializer, this.accumulators), + toMapValue(serializer, this.groups) + ] + }; + } +} + +/** + * @beta + */ +export class Distinct implements Stage { + name = 'distinct'; + + constructor(private groups: Map) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toMapValue(serializer, this.groups)] + }; + } +} + +/** + * @beta + */ +export class CollectionSource implements Stage { + name = 'collection'; + + constructor(private collectionPath: string) { + if (!this.collectionPath.startsWith('/')) { + this.collectionPath = '/' + this.collectionPath; + } + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [{ referenceValue: this.collectionPath }] + }; + } +} + +/** + * @beta + */ +export class CollectionGroupSource implements Stage { + name = 'collection_group'; + + constructor(private collectionId: string) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [{ referenceValue: '' }, { stringValue: this.collectionId }] + }; + } +} + +/** + * @beta + */ +export class DatabaseSource implements Stage { + name = 'database'; + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name + }; + } +} + +/** + * @beta + */ +export class DocumentsSource implements Stage { + name = 'documents'; + + constructor(private docPaths: string[]) {} + + static of(refs: Array): DocumentsSource { + return new DocumentsSource( + refs.map(ref => + ref instanceof DocumentReference + ? '/' + ref.path + : ref.startsWith('/') + ? ref + : '/' + ref + ) + ); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.docPaths.map(p => { + return { referenceValue: p }; + }) + }; + } +} + +/** + * @beta + */ +export class Where implements Stage { + name = 'where'; + + constructor(private condition: BooleanExpr) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [(this.condition as unknown as Expr)._toProto(serializer)] + }; + } +} + +/** + * @beta + */ +export interface FindNearestOptions { + field: Field | string; + vectorValue: VectorValue | number[]; + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + limit?: number; + distanceField?: string; +} + +/** + * @beta + */ +export class FindNearest implements Stage { + name = 'find_nearest'; + + /** + * @private + * @internal + * + * @param _field + * @param _vectorValue + * @param _distanceMeasure + * @param _limit + * @param _distanceField + */ + constructor( + private _field: Field, + private _vectorValue: ObjectValue, + private _distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', + private _limit?: number, + private _distanceField?: string + ) {} + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + const options: { [k: string]: ProtoValue } = {}; + + if (this._limit) { + options.limit = toNumber(serializer, this._limit)!; + } + + if (this._distanceField) { + // eslint-disable-next-line camelcase + options.distance_field = field(this._distanceField)._toProto(serializer); + } + + return { + name: this.name, + args: [ + this._field._toProto(serializer), + this._vectorValue.value, + toStringValue(this._distanceMeasure) + ], + options + }; + } +} + +/** + * @beta + */ +export class Limit implements Stage { + name = 'limit'; + + constructor(readonly limit: number) { + hardAssert( + !isNaN(limit) && limit !== Infinity && limit !== -Infinity, + 0x882c, + 'Invalid limit value' + ); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toNumber(serializer, this.limit)] + }; + } +} + +/** + * @beta + */ +export class Offset implements Stage { + name = 'offset'; + + constructor(private offset: number) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toNumber(serializer, this.offset)] + }; + } +} + +/** + * @beta + */ +export class Select implements Stage { + name = 'select'; + + constructor(private projections: Map) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toMapValue(serializer, this.projections)] + }; + } +} + +/** + * @beta + */ +export class Sort implements Stage { + name = 'sort'; + + constructor(private orders: Ordering[]) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.orders.map(o => o._toProto(serializer)) + }; + } +} + +/** + * @beta + */ +export class Sample implements Stage { + name = 'sample'; + + constructor(private limit: number, private mode: string) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toNumber(serializer, this.limit)!, toStringValue(this.mode)!] + }; + } +} + +/** + * @beta + */ +export class Union implements Stage { + name = 'union'; + + constructor(private _other: Pipeline) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toPipelineValue(this._other._toProto(serializer))] + }; + } +} + +/** + * @beta + */ +export class Unnest implements Stage { + name = 'unnest'; + constructor( + private expr: Expr, + private alias: Field, + private indexField?: Field + ) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + const stageProto: ProtoStage = { + name: this.name, + args: [this.expr._toProto(serializer), this.alias._toProto(serializer)] + }; + + if (this.indexField) { + stageProto.options = { + ['index_field']: this.indexField._toProto(serializer) + }; + } + + return stageProto; + } +} + +/** + * @beta + */ +export class Replace implements Stage { + name = 'replace_with'; + + constructor( + private field: Expr, + private mode: + | 'full_replace' + | 'merge_prefer_nest' + | 'merge_prefer_parent' = 'full_replace' + ) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [this.field._toProto(serializer), toStringValue(this.mode)] + }; + } +} + +/** + * @beta + */ +export class GenericStage implements Stage { + /** + * @private + * @internal + */ + constructor( + public name: string, + private params: Array + ) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.params.map(o => o._toProto(serializer)) + }; + } +} diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index a3022be627e..905b13edd6f 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -22,7 +22,7 @@ import { } from '@firebase/firestore-types'; import { Compat, deepEqual, getModularInstance } from '@firebase/util'; -import { ParseContext } from '../api/parse_context'; +import { ContextSettings, ParseContext } from '../api/parse_context'; import { DatabaseId } from '../core/database_info'; import { DocumentKey } from '../model/document_key'; import { FieldMask } from '../model/field_mask'; @@ -56,7 +56,8 @@ import { JsonProtoSerializer, toBytes, toResourceName, - toTimestamp + toTimestamp, + isProtoValueSerializable } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; @@ -181,33 +182,6 @@ function isWrite(dataSource: UserDataSource): boolean { } } -/** Contains the settings that are mutated as we parse user data. */ -interface ContextSettings { - /** Indicates what kind of API method this data came from. */ - readonly dataSource: UserDataSource; - /** The name of the method the user called to create the ParseContext. */ - readonly methodName: string; - /** The document the user is attempting to modify, if that applies. */ - readonly targetDoc?: DocumentKey; - /** - * A path within the object being parsed. This could be an empty path (in - * which case the context represents the root of the data being parsed), or a - * nonempty path (indicating the context represents a nested location within - * the data). - */ - readonly path?: InternalFieldPath; - /** - * Whether or not this context corresponds to an element of an array. - * If not set, elements are treated as if they were outside of arrays. - */ - readonly arrayElement?: boolean; - /** - * Whether or not a converter was specified in this context. If true, error - * messages will reference the converter when invalid data is provided. - */ - readonly hasConverter?: boolean; -} - /** A "context" object passed around while parsing user data. */ class ParseContextImpl implements ParseContext { readonly fieldTransforms: FieldTransform[]; @@ -731,7 +705,7 @@ export function parseQueryValue( */ export function parseData( input: unknown, - context: ParseContextImpl + context: ParseContext ): ProtoValue | null { // Unwrap the API type from the Compat SDK. This will return the API type // from firestore-exp. @@ -782,7 +756,7 @@ export function parseData( export function parseObject( obj: Dict, - context: ParseContextImpl + context: ParseContext ): { mapValue: ProtoMapValue } { const fields: Dict = {}; @@ -804,7 +778,7 @@ export function parseObject( return { mapValue: { fields } }; } -function parseArray(array: unknown[], context: ParseContextImpl): ProtoValue { +function parseArray(array: unknown[], context: ParseContext): ProtoValue { const values: ProtoValue[] = []; let entryIndex = 0; for (const entry of array) { @@ -829,7 +803,7 @@ function parseArray(array: unknown[], context: ParseContextImpl): ProtoValue { */ function parseSentinelFieldValue( value: FieldValue, - context: ParseContextImpl + context: ParseContext ): void { // Sentinels are only supported with writes, and not within arrays. if (!isWrite(context.dataSource)) { @@ -854,9 +828,9 @@ function parseSentinelFieldValue( * * @returns The parsed value */ -function parseScalarValue( +export function parseScalarValue( value: unknown, - context: ParseContextImpl + context: ParseContext ): ProtoValue | null { value = getModularInstance(value); @@ -911,6 +885,8 @@ function parseScalarValue( }; } else if (value instanceof VectorValue) { return parseVectorValue(value, context); + } else if (isProtoValueSerializable(value)) { + return value._toProto(context.serializer); } else { throw context.createError( `Unsupported field value: ${valueDescription(value)}` @@ -922,9 +898,10 @@ function parseScalarValue( * Creates a new VectorValue proto value (using the internal format). */ export function parseVectorValue( - value: VectorValue, - context: ParseContextImpl -): ProtoValue { + value: VectorValue | number[], + context: ParseContext +): { mapValue: ProtoMapValue } { + const values = value instanceof VectorValue ? value.toArray() : value; const mapValue: ProtoMapValue = { fields: { [TYPE_KEY]: { @@ -932,7 +909,7 @@ export function parseVectorValue( }, [VECTOR_MAP_VECTORS_KEY]: { arrayValue: { - values: value.toArray().map(value => { + values: values.map(value => { if (typeof value !== 'number') { throw context.createError( 'VectorValues must only contain numeric values.' @@ -956,7 +933,7 @@ export function parseVectorValue( * GeoPoints, etc. are not considered to look like JSON objects since they map * to specific FieldValue types other than ObjectValue. */ -function looksLikeJsonObject(input: unknown): boolean { +export function looksLikeJsonObject(input: unknown): boolean { return ( typeof input === 'object' && input !== null && @@ -967,13 +944,14 @@ function looksLikeJsonObject(input: unknown): boolean { !(input instanceof Bytes) && !(input instanceof DocumentReference) && !(input instanceof FieldValue) && - !(input instanceof VectorValue) + !(input instanceof VectorValue) && + !isProtoValueSerializable(input) ); } function validatePlainObject( message: string, - context: ParseContextImpl, + context: ParseContext, input: unknown ): asserts input is Dict { if (!looksLikeJsonObject(input) || !isPlainObject(input)) { diff --git a/packages/firestore/src/model/aggregate_result_value.ts b/packages/firestore/src/model/aggregate_result_value.ts new file mode 100644 index 00000000000..042dc29d345 --- /dev/null +++ b/packages/firestore/src/model/aggregate_result_value.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + MapValue as ProtoMapValue, + Value as ProtoValue +} from '../protos/firestore_proto_api'; + +import { valueEquals } from './values'; + +/** + * An AggregateResultValue represents a MapValue in the Firestore Proto. + */ +export class AggregateResultValue { + constructor(readonly value: { mapValue: ProtoMapValue }) {} + + static empty(): AggregateResultValue { + return new AggregateResultValue({ mapValue: {} }); + } + + aggregate(alias: string): ProtoValue | null { + return this.value.mapValue.fields?.[alias] ?? null; + } + + isEqual(other: AggregateResultValue): boolean { + return valueEquals(this.value, other.value); + } +} diff --git a/packages/firestore/src/model/pipeline_stream_element.ts b/packages/firestore/src/model/pipeline_stream_element.ts new file mode 100644 index 00000000000..efa27e2cc44 --- /dev/null +++ b/packages/firestore/src/model/pipeline_stream_element.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SnapshotVersion } from '../core/snapshot_version'; + +import { DocumentKey } from './document_key'; +import { ObjectValue } from './object_value'; + +export interface PipelineStreamElement { + transaction?: string; + key?: DocumentKey; + executionTime?: SnapshotVersion; + createTime?: SnapshotVersion; + updateTime?: SnapshotVersion; + fields?: ObjectValue; +} diff --git a/packages/firestore/src/protos/compile.sh b/packages/firestore/src/protos/compile.sh index 26c46d1a40d..9f9fb4b3217 100755 --- a/packages/firestore/src/protos/compile.sh +++ b/packages/firestore/src/protos/compile.sh @@ -18,10 +18,17 @@ set -euo pipefail # Variables PROTOS_DIR="." -PBJS="$(npm bin)/pbjs" +PBJS="../../node_modules/.bin/pbjs" +PBTS="../../node_modules/.bin/pbts" -"${PBJS}" --proto_path=. --target=json -o protos.json \ - -r firestore_v1 \ - "${PROTOS_DIR}/google/firestore/v1/*.proto" \ +"${PBJS}" --path=. --target=json -o protos.json \ + -r firestore/v1 "${PROTOS_DIR}/google/firestore/v1/*.proto" \ "${PROTOS_DIR}/google/protobuf/*.proto" "${PROTOS_DIR}/google/type/*.proto" \ "${PROTOS_DIR}/google/rpc/*.proto" "${PROTOS_DIR}/google/api/*.proto" + +"${PBJS}" --path="${PROTOS_DIR}" --target=static -o temp.js \ + -r firestore/v1 "${PROTOS_DIR}/google/firestore/v1/*.proto" \ + "${PROTOS_DIR}/google/protobuf/*.proto" "${PROTOS_DIR}/google/type/*.proto" \ + "${PROTOS_DIR}/google/rpc/*.proto" "${PROTOS_DIR}/google/api/*.proto" + +"${PBTS}" -o temp.d.ts --no-comments temp.js diff --git a/packages/firestore/src/protos/firestore_proto_api.ts b/packages/firestore/src/protos/firestore_proto_api.ts index 9618d71b86a..cc1c57259f5 100644 --- a/packages/firestore/src/protos/firestore_proto_api.ts +++ b/packages/firestore/src/protos/firestore_proto_api.ts @@ -145,9 +145,21 @@ export interface IValueNullValueEnum { } export declare const ValueNullValueEnum: IValueNullValueEnum; export declare namespace firestoreV1ApiClientInterfaces { + interface Aggregation { + count?: Count; + sum?: Sum; + avg?: Avg; + alias?: string; + } + interface AggregationResult { + aggregateFields?: ApiClientObjectMap; + } interface ArrayValue { values?: Value[]; } + interface Avg { + field?: FieldReference; + } interface BatchGetDocumentsRequest { database?: string; documents?: string[]; @@ -168,6 +180,14 @@ export declare namespace firestoreV1ApiClientInterfaces { interface BeginTransactionResponse { transaction?: string; } + interface BitSequence { + bitmap?: string | Uint8Array; + padding?: number; + } + interface BloomFilter { + bits?: BitSequence; + hashCount?: number; + } interface CollectionSelector { collectionId?: string; allDescendants?: boolean; @@ -185,6 +205,9 @@ export declare namespace firestoreV1ApiClientInterfaces { op?: CompositeFilterOp; filters?: Filter[]; } + interface Count { + upTo?: number; + } interface Cursor { values?: Value[]; before?: boolean; @@ -221,19 +244,23 @@ export declare namespace firestoreV1ApiClientInterfaces { documents?: string[]; } interface Empty {} + interface ExecutePipelineRequest { + database?: string; + structuredPipeline?: StructuredPipeline; + transaction?: string; + newTransaction?: TransactionOptions; + readTime?: string; + } + interface ExecutePipelineResponse { + transaction?: string; + results?: Document[]; + executionTime?: string; + } interface ExistenceFilter { targetId?: number; count?: number; unchangedNames?: BloomFilter; } - interface BloomFilter { - bits?: BitSequence; - hashCount?: number; - } - interface BitSequence { - bitmap?: string | Uint8Array; - padding?: number; - } interface FieldFilter { field?: FieldReference; op?: FieldFilterOp; @@ -254,6 +281,11 @@ export declare namespace firestoreV1ApiClientInterfaces { fieldFilter?: FieldFilter; unaryFilter?: UnaryFilter; } + interface Function { + name?: string; + args?: Value[]; + options?: ApiClientObjectMap; + } interface Index { name?: string; collectionId?: string; @@ -310,6 +342,9 @@ export declare namespace firestoreV1ApiClientInterfaces { field?: FieldReference; direction?: OrderDirection; } + interface Pipeline { + stages?: Stage[]; + } interface Precondition { exists?: boolean; updateTime?: Timestamp; @@ -355,33 +390,24 @@ export declare namespace firestoreV1ApiClientInterfaces { transaction?: string; readTime?: string; } - interface AggregationResult { - aggregateFields?: ApiClientObjectMap; - } interface StructuredAggregationQuery { structuredQuery?: StructuredQuery; aggregations?: Aggregation[]; } - interface Aggregation { - count?: Count; - sum?: Sum; - avg?: Avg; - alias?: string; - } - interface Count { - upTo?: number; - } - interface Sum { - field?: FieldReference; - } - interface Avg { - field?: FieldReference; + interface Stage { + name?: string; + args?: Value[]; + options?: ApiClientObjectMap; } interface Status { code?: number; message?: string; details?: Array>; } + interface StructuredPipeline { + pipeline?: Pipeline; + options?: ApiClientObjectMap; + } interface StructuredQuery { select?: Projection; from?: CollectionSelector[]; @@ -392,6 +418,9 @@ export declare namespace firestoreV1ApiClientInterfaces { offset?: number; limit?: number | { value: number }; } + interface Sum { + field?: FieldReference; + } interface Target { query?: QueryTarget; documents?: DocumentsTarget; @@ -428,6 +457,10 @@ export declare namespace firestoreV1ApiClientInterfaces { geoPointValue?: LatLng; arrayValue?: ArrayValue; mapValue?: MapValue; + fieldReferenceValue?: string; + // eslint-disable-next-line @typescript-eslint/ban-types + functionValue?: Function; + pipelineValue?: Pipeline; } interface Write { update?: Document; @@ -489,12 +522,17 @@ export declare type DocumentsTarget = export declare type Empty = firestoreV1ApiClientInterfaces.Empty; export declare type ExistenceFilter = firestoreV1ApiClientInterfaces.ExistenceFilter; +export declare type ExecutePipelineRequest = + firestoreV1ApiClientInterfaces.ExecutePipelineRequest; +export declare type ExecutePipelineResponse = + firestoreV1ApiClientInterfaces.ExecutePipelineResponse; export declare type FieldFilter = firestoreV1ApiClientInterfaces.FieldFilter; export declare type FieldReference = firestoreV1ApiClientInterfaces.FieldReference; export declare type FieldTransform = firestoreV1ApiClientInterfaces.FieldTransform; export declare type Filter = firestoreV1ApiClientInterfaces.Filter; +export declare type Function = firestoreV1ApiClientInterfaces.Function; export declare type Index = firestoreV1ApiClientInterfaces.Index; export declare type IndexField = firestoreV1ApiClientInterfaces.IndexField; export declare type LatLng = firestoreV1ApiClientInterfaces.LatLng; @@ -513,6 +551,7 @@ export declare type ListenResponse = export declare type MapValue = firestoreV1ApiClientInterfaces.MapValue; export declare type Operation = firestoreV1ApiClientInterfaces.Operation; export declare type Order = firestoreV1ApiClientInterfaces.Order; +export declare type Pipeline = firestoreV1ApiClientInterfaces.Pipeline; export declare type Precondition = firestoreV1ApiClientInterfaces.Precondition; export declare type Projection = firestoreV1ApiClientInterfaces.Projection; export declare type QueryTarget = firestoreV1ApiClientInterfaces.QueryTarget; @@ -529,9 +568,12 @@ export declare type RunAggregationQueryRequest = export declare type Aggregation = firestoreV1ApiClientInterfaces.Aggregation; export declare type RunAggregationQueryResponse = firestoreV1ApiClientInterfaces.RunAggregationQueryResponse; +export declare type Stage = firestoreV1ApiClientInterfaces.Stage; export declare type Status = firestoreV1ApiClientInterfaces.Status; export declare type StructuredQuery = firestoreV1ApiClientInterfaces.StructuredQuery; +export declare type StructuredPipeline = + firestoreV1ApiClientInterfaces.StructuredPipeline; export declare type Target = firestoreV1ApiClientInterfaces.Target; export declare type TargetChange = firestoreV1ApiClientInterfaces.TargetChange; export declare type TransactionOptions = diff --git a/packages/firestore/src/protos/google/api/launch_stage.proto b/packages/firestore/src/protos/google/api/launch_stage.proto new file mode 100644 index 00000000000..9863fc23d42 --- /dev/null +++ b/packages/firestore/src/protos/google/api/launch_stage.proto @@ -0,0 +1,72 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option go_package = "google.golang.org/genproto/googleapis/api;api"; +option java_multiple_files = true; +option java_outer_classname = "LaunchStageProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// The launch stage as defined by [Google Cloud Platform +// Launch Stages](https://cloud.google.com/terms/launch-stages). +enum LaunchStage { + // Do not use this default value. + LAUNCH_STAGE_UNSPECIFIED = 0; + + // The feature is not yet implemented. Users can not use it. + UNIMPLEMENTED = 6; + + // Prelaunch features are hidden from users and are only visible internally. + PRELAUNCH = 7; + + // Early Access features are limited to a closed group of testers. To use + // these features, you must sign up in advance and sign a Trusted Tester + // agreement (which includes confidentiality provisions). These features may + // be unstable, changed in backward-incompatible ways, and are not + // guaranteed to be released. + EARLY_ACCESS = 1; + + // Alpha is a limited availability test for releases before they are cleared + // for widespread use. By Alpha, all significant design issues are resolved + // and we are in the process of verifying functionality. Alpha customers + // need to apply for access, agree to applicable terms, and have their + // projects allowlisted. Alpha releases don't have to be feature complete, + // no SLAs are provided, and there are no technical support obligations, but + // they will be far enough along that customers can actually use them in + // test environments or for limited-use tests -- just like they would in + // normal production cases. + ALPHA = 2; + + // Beta is the point at which we are ready to open a release for any + // customer to use. There are no SLA or technical support obligations in a + // Beta release. Products will be complete from a feature perspective, but + // may have some open outstanding issues. Beta releases are suitable for + // limited production use cases. + BETA = 3; + + // GA features are open to all developers and are considered stable and + // fully qualified for production use. + GA = 4; + + // Deprecated features are scheduled to be shut down and removed. For more + // information, see the "Deprecation Policy" section of our [Terms of + // Service](https://cloud.google.com/terms/) + // and the [Google Cloud Platform Subject to the Deprecation + // Policy](https://cloud.google.com/terms/deprecation) documentation. + DEPRECATED = 5; +} diff --git a/packages/firestore/src/protos/google/firestore/v1/document.proto b/packages/firestore/src/protos/google/firestore/v1/document.proto index 5238a943ce4..f7605750502 100644 --- a/packages/firestore/src/protos/google/firestore/v1/document.proto +++ b/packages/firestore/src/protos/google/firestore/v1/document.proto @@ -129,6 +129,48 @@ message Value { // A map value. MapValue map_value = 6; + // Value which references a field. + // + // This is considered relative (vs absolute) since it only refers to a field + // and not a field within a particular document. + // + // **Requires:** + // + // * Must follow [field reference][FieldReference.field_path] limitations. + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): long term, there is no reason this type should not be + // allowed to be used on the write path. --) + string field_reference_value = 19; + + // A value that represents an unevaluated expression. + // + // **Requires:** + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): similar to above, there is no reason to not allow + // storing expressions into the database, just no plan to support in + // the near term. + // + // This would actually be an interesting way to represent user-defined + // functions or more expressive rules-based systems. --) + Function function_value = 20; + + // A value that represents an unevaluated pipeline. + // + // **Requires:** + // + // * Not allowed to be used when writing documents. + // + // (-- NOTE(batchik): similar to above, there is no reason to not allow + // storing expressions into the database, just no plan to support in + // the near term. + // + // This would actually be an interesting way to represent user-defined + // functions or more expressive rules-based systems. --) + Pipeline pipeline_value = 21; } } @@ -148,3 +190,73 @@ message MapValue { // not exceed 1,500 bytes and cannot be empty. map fields = 1; } + + +// Represents an unevaluated scalar expression. +// +// For example, the expression `like(user_name, "%alice%")` is represented as: +// +// ``` +// name: "like" +// args { field_reference: "user_name" } +// args { string_value: "%alice%" } +// ``` +// +// (-- api-linter: core::0123::resource-annotation=disabled +// aip.dev/not-precedent: this is not a One Platform API resource. --) +message Function { + // The name of the function to evaluate. + // + // **Requires:** + // + // * must be in snake case (lower case with underscore separator). + // + string name = 1; + + // Ordered list of arguments the given function expects. + repeated Value args = 2; + + // Optional named arguments that certain functions may support. + map options = 3; +} + +// A Firestore query represented as an ordered list of operations / stages. +message Pipeline { + // A single operation within a pipeline. + // + // A stage is made up of a unique name, and a list of arguments. The exact + // number of arguments & types is dependent on the stage type. + // + // To give an example, the stage `filter(state = "MD")` would be encoded as: + // + // ``` + // name: "filter" + // args { + // function_value { + // name: "eq" + // args { field_reference_value: "state" } + // args { string_value: "MD" } + // } + // } + // ``` + // + // See public documentation for the full list. + message Stage { + // The name of the stage to evaluate. + // + // **Requires:** + // + // * must be in snake case (lower case with underscore separator). + // + string name = 1; + + // Ordered list of arguments the given stage expects. + repeated Value args = 2; + + // Optional named arguments that certain functions may support. + map options = 3; + } + + // Ordered list of stages to evaluate. + repeated Stage stages = 1; +} diff --git a/packages/firestore/src/protos/google/firestore/v1/firestore.proto b/packages/firestore/src/protos/google/firestore/v1/firestore.proto index a8fc0d54b51..3e7b62e0609 100644 --- a/packages/firestore/src/protos/google/firestore/v1/firestore.proto +++ b/packages/firestore/src/protos/google/firestore/v1/firestore.proto @@ -22,6 +22,7 @@ import "google/api/field_behavior.proto"; import "google/firestore/v1/aggregation_result.proto"; import "google/firestore/v1/common.proto"; import "google/firestore/v1/document.proto"; +import "google/firestore/v1/pipeline.proto"; import "google/firestore/v1/query.proto"; import "google/firestore/v1/write.proto"; import "google/protobuf/empty.proto"; @@ -135,6 +136,15 @@ service Firestore { }; } + // Executes a pipeline query. + rpc ExecutePipeline(ExecutePipelineRequest) + returns (stream ExecutePipelineResponse) { + option (google.api.http) = { + post: "/v1/{database=projects/*/databases/*}/documents:executePipeline" + body: "*" + }; + } + // Runs an aggregation query. // // Rather than producing [Document][google.firestore.v1.Document] results like [Firestore.RunQuery][google.firestore.v1.Firestore.RunQuery], @@ -157,7 +167,7 @@ service Firestore { } }; } - + // Partitions a query by returning partition cursors that can be used to run // the query in parallel. The returned partition cursors are split points that // can be used by RunQuery as starting/end points for the query results. @@ -547,6 +557,82 @@ message RunQueryResponse { int32 skipped_results = 4; } +// The request for [Firestore.ExecutePipeline][]. +message ExecutePipelineRequest { + // Database identifier, in the form `projects/{project}/databases/{database}`. + string database = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + + oneof pipeline_type { + // A pipelined operation. + StructuredPipeline structured_pipeline = 2; + } + + // Optional consistency arguments, defaults to strong consistency. + oneof consistency_selector { + // Run the query within an already active transaction. + // + // The value here is the opaque transaction ID to execute the query in. + bytes transaction = 5; + + // Execute the pipeline in a new transaction. + // + // The identifier of the newly created transaction will be returned in the + // first response on the stream. This defaults to a read-only transaction. + TransactionOptions new_transaction = 6; + + // Execute the pipeline in a snapshot transaction at the given time. + // + // This must be a microsecond precision timestamp within the past one hour, + // or if Point-in-Time Recovery is enabled, can additionally be a whole + // minute timestamp within the past 7 days. + google.protobuf.Timestamp read_time = 7; + } + + // Explain / analyze options for the pipeline. + // ExplainOptions explain_options = 8 [(google.api.field_behavior) = OPTIONAL]; +} + +// The response for [Firestore.Execute][]. +message ExecutePipelineResponse { + // Newly created transaction identifier. + // + // This field is only specified on the first response from the server when + // the request specified [ExecuteRequest.new_transaction][]. + bytes transaction = 1; + + // An ordered batch of results returned executing a pipeline. + // + // The batch size is variable, and can even be zero for when only a partial + // progress message is returned. + // + // The fields present in the returned documents are only those that were + // explicitly requested in the pipeline, this include those like + // [`__name__`][Document.name] & [`__update_time__`][Document.update_time]. + // This is explicitly a divergence from `Firestore.RunQuery` / + // `Firestore.GetDocument` RPCs which always return such fields even when they + // are not specified in the [`mask`][DocumentMask]. + repeated Document results = 2; + + // The time at which the document(s) were read. + // + // This may be monotonically increasing; in this case, the previous documents + // in the result stream are guaranteed not to have changed between their + // `execution_time` and this one. + // + // If the query returns no results, a response with `execution_time` and no + // `results` will be sent, and this represents the time at which the operation + // was run. + google.protobuf.Timestamp execution_time = 3; + + // Query explain metrics. + // + // Set on the last response when [ExecutePipelineRequest.explain_options][] + // was specified on the request. + // ExplainMetrics explain_metrics = 4; +} + // The request for [Firestore.RunAggregationQuery][google.firestore.v1.Firestore.RunAggregationQuery]. message RunAggregationQueryRequest { // Required. The parent resource name. In the format: diff --git a/packages/firestore/src/protos/google/firestore/v1/pipeline.proto b/packages/firestore/src/protos/google/firestore/v1/pipeline.proto new file mode 100644 index 00000000000..ea5b2309331 --- /dev/null +++ b/packages/firestore/src/protos/google/firestore/v1/pipeline.proto @@ -0,0 +1,41 @@ +/*! + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; +package google.firestore.v1; +import "google/firestore/v1/document.proto"; +option csharp_namespace = "Google.Cloud.Firestore.V1"; +option php_namespace = "Google\\Cloud\\Firestore\\V1"; +option ruby_package = "Google::Cloud::Firestore::V1"; +option java_multiple_files = true; +option java_package = "com.google.firestore.v1"; +option java_outer_classname = "PipelineProto"; +option objc_class_prefix = "GCFS"; +// A Firestore query represented as an ordered list of operations / stages. +// +// This is considered the top-level function which plans & executes a query. +// It is logically equivalent to `query(stages, options)`, but prevents the +// client from having to build a function wrapper. +message StructuredPipeline { + // The pipeline query to execute. + Pipeline pipeline = 1; + // Optional query-level arguments. + // + // (-- Think query statement hints. --) + // + // (-- TODO(batchik): define the api contract of using an unsupported hint --) + map options = 2; +} diff --git a/packages/firestore/src/protos/google/firestore/v1/query_profile.proto b/packages/firestore/src/protos/google/firestore/v1/query_profile.proto new file mode 100644 index 00000000000..de27144a038 --- /dev/null +++ b/packages/firestore/src/protos/google/firestore/v1/query_profile.proto @@ -0,0 +1,92 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.firestore.v1; + +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1"; +option go_package = "cloud.google.com/go/firestore/apiv1/firestorepb;firestorepb"; +option java_multiple_files = true; +option java_outer_classname = "QueryProfileProto"; +option java_package = "com.google.firestore.v1"; +option objc_class_prefix = "GCFS"; +option php_namespace = "Google\\Cloud\\Firestore\\V1"; +option ruby_package = "Google::Cloud::Firestore::V1"; + +// Specification of the Firestore Query Profile fields. + +// Explain options for the query. +message ExplainOptions { + // Optional. Whether to execute this query. + // + // When false (the default), the query will be planned, returning only + // metrics from the planning stages. + // + // When true, the query will be planned and executed, returning the full + // query results along with both planning and execution stage metrics. + bool analyze = 1 [(google.api.field_behavior) = OPTIONAL]; +} + +// Explain metrics for the query. +message ExplainMetrics { + // Planning phase information for the query. + PlanSummary plan_summary = 1; + + // Aggregated stats from the execution of the query. Only present when + // [ExplainOptions.analyze][google.firestore.v1.ExplainOptions.analyze] is set + // to true. + ExecutionStats execution_stats = 2; +} + +// Planning phase information for the query. +message PlanSummary { + // The indexes selected for the query. For example: + // [ + // {"query_scope": "Collection", "properties": "(foo ASC, __name__ ASC)"}, + // {"query_scope": "Collection", "properties": "(bar ASC, __name__ ASC)"} + // ] + repeated google.protobuf.Struct indexes_used = 1; +} + +// Execution statistics for the query. +message ExecutionStats { + // Total number of results returned, including documents, projections, + // aggregation results, keys. + int64 results_returned = 1; + + // Total time to execute the query in the backend. + google.protobuf.Duration execution_duration = 3; + + // Total billable read operations. + int64 read_operations = 4; + + // Debugging statistics from the execution of the query. Note that the + // debugging stats are subject to change as Firestore evolves. It could + // include: + // { + // "indexes_entries_scanned": "1000", + // "documents_scanned": "20", + // "billing_details" : { + // "documents_billable": "20", + // "index_entries_billable": "1000", + // "min_query_cost": "0" + // } + // } + google.protobuf.Struct debug_stats = 5; +} diff --git a/packages/firestore/src/protos/protos.json b/packages/firestore/src/protos/protos.json index b2c50ccaeeb..5b73c4647f8 100644 --- a/packages/firestore/src/protos/protos.json +++ b/packages/firestore/src/protos/protos.json @@ -4,16 +4,78 @@ "nested": { "protobuf": { "options": { - "csharp_namespace": "Google.Protobuf.WellKnownTypes", - "go_package": "github.com/golang/protobuf/ptypes/wrappers", + "go_package": "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor", "java_package": "com.google.protobuf", - "java_outer_classname": "WrappersProto", - "java_multiple_files": true, + "java_outer_classname": "DescriptorProtos", + "csharp_namespace": "Google.Protobuf.Reflection", "objc_class_prefix": "GPB", "cc_enable_arenas": true, "optimize_for": "SPEED" }, "nested": { + "Struct": { + "fields": { + "fields": { + "keyType": "string", + "type": "Value", + "id": 1 + } + } + }, + "Value": { + "oneofs": { + "kind": { + "oneof": [ + "nullValue", + "numberValue", + "stringValue", + "boolValue", + "structValue", + "listValue" + ] + } + }, + "fields": { + "nullValue": { + "type": "NullValue", + "id": 1 + }, + "numberValue": { + "type": "double", + "id": 2 + }, + "stringValue": { + "type": "string", + "id": 3 + }, + "boolValue": { + "type": "bool", + "id": 4 + }, + "structValue": { + "type": "Struct", + "id": 5 + }, + "listValue": { + "type": "ListValue", + "id": 6 + } + } + }, + "NullValue": { + "values": { + "NULL_VALUE": 0 + } + }, + "ListValue": { + "fields": { + "values": { + "rule": "repeated", + "type": "Value", + "id": 1 + } + } + }, "Timestamp": { "fields": { "seconds": { @@ -161,6 +223,10 @@ "end": { "type": "int32", "id": 2 + }, + "options": { + "type": "ExtensionRangeOptions", + "id": 3 } } }, @@ -178,6 +244,21 @@ } } }, + "ExtensionRangeOptions": { + "fields": { + "uninterpretedOption": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [ + [ + 1000, + 536870911 + ] + ] + }, "FieldDescriptorProto": { "fields": { "name": { @@ -279,6 +360,30 @@ "options": { "type": "EnumOptions", "id": 3 + }, + "reservedRange": { + "rule": "repeated", + "type": "EnumReservedRange", + "id": 4 + }, + "reservedName": { + "rule": "repeated", + "type": "string", + "id": 5 + } + }, + "nested": { + "EnumReservedRange": { + "fields": { + "start": { + "type": "int32", + "id": 1 + }, + "end": { + "type": "int32", + "id": 2 + } + } } } }, @@ -335,11 +440,17 @@ }, "clientStreaming": { "type": "bool", - "id": 5 + "id": 5, + "options": { + "default": false + } }, "serverStreaming": { "type": "bool", - "id": 6 + "id": 6, + "options": { + "default": false + } } } }, @@ -355,7 +466,10 @@ }, "javaMultipleFiles": { "type": "bool", - "id": 10 + "id": 10, + "options": { + "default": false + } }, "javaGenerateEqualsAndHash": { "type": "bool", @@ -366,7 +480,10 @@ }, "javaStringCheckUtf8": { "type": "bool", - "id": 27 + "id": 27, + "options": { + "default": false + } }, "optimizeFor": { "type": "OptimizeMode", @@ -381,23 +498,45 @@ }, "ccGenericServices": { "type": "bool", - "id": 16 + "id": 16, + "options": { + "default": false + } }, "javaGenericServices": { "type": "bool", - "id": 17 + "id": 17, + "options": { + "default": false + } }, "pyGenericServices": { "type": "bool", - "id": 18 + "id": 18, + "options": { + "default": false + } + }, + "phpGenericServices": { + "type": "bool", + "id": 42, + "options": { + "default": false + } }, "deprecated": { "type": "bool", - "id": 23 + "id": 23, + "options": { + "default": false + } }, "ccEnableArenas": { "type": "bool", - "id": 31 + "id": 31, + "options": { + "default": false + } }, "objcClassPrefix": { "type": "string", @@ -407,6 +546,26 @@ "type": "string", "id": 37 }, + "swiftPrefix": { + "type": "string", + "id": 39 + }, + "phpClassPrefix": { + "type": "string", + "id": 40 + }, + "phpNamespace": { + "type": "string", + "id": 41 + }, + "phpMetadataNamespace": { + "type": "string", + "id": 44 + }, + "rubyPackage": { + "type": "string", + "id": 45 + }, "uninterpretedOption": { "rule": "repeated", "type": "UninterpretedOption", @@ -439,15 +598,24 @@ "fields": { "messageSetWireFormat": { "type": "bool", - "id": 1 + "id": 1, + "options": { + "default": false + } }, "noStandardDescriptorAccessor": { "type": "bool", - "id": 2 + "id": 2, + "options": { + "default": false + } }, "deprecated": { "type": "bool", - "id": 3 + "id": 3, + "options": { + "default": false + } }, "mapEntry": { "type": "bool", @@ -469,6 +637,10 @@ [ 8, 8 + ], + [ + 9, + 9 ] ] }, @@ -494,15 +666,24 @@ }, "lazy": { "type": "bool", - "id": 5 + "id": 5, + "options": { + "default": false + } }, "deprecated": { "type": "bool", - "id": 3 + "id": 3, + "options": { + "default": false + } }, "weak": { "type": "bool", - "id": 10 + "id": 10, + "options": { + "default": false + } }, "uninterpretedOption": { "rule": "repeated", @@ -562,7 +743,10 @@ }, "deprecated": { "type": "bool", - "id": 3 + "id": 3, + "options": { + "default": false + } }, "uninterpretedOption": { "rule": "repeated", @@ -575,13 +759,22 @@ 1000, 536870911 ] + ], + "reserved": [ + [ + 5, + 5 + ] ] }, "EnumValueOptions": { "fields": { "deprecated": { "type": "bool", - "id": 1 + "id": 1, + "options": { + "default": false + } }, "uninterpretedOption": { "rule": "repeated", @@ -600,7 +793,10 @@ "fields": { "deprecated": { "type": "bool", - "id": 33 + "id": 33, + "options": { + "default": false + } }, "uninterpretedOption": { "rule": "repeated", @@ -619,7 +815,17 @@ "fields": { "deprecated": { "type": "bool", - "id": 33 + "id": 33, + "options": { + "default": false + } + }, + "idempotencyLevel": { + "type": "IdempotencyLevel", + "id": 34, + "options": { + "default": "IDEMPOTENCY_UNKNOWN" + } }, "uninterpretedOption": { "rule": "repeated", @@ -632,7 +838,16 @@ 1000, 536870911 ] - ] + ], + "nested": { + "IdempotencyLevel": { + "values": { + "IDEMPOTENCY_UNKNOWN": 0, + "NO_SIDE_EFFECTS": 1, + "IDEMPOTENT": 2 + } + } + } }, "UninterpretedOption": { "fields": { @@ -753,72 +968,6 @@ } } }, - "Struct": { - "fields": { - "fields": { - "keyType": "string", - "type": "Value", - "id": 1 - } - } - }, - "Value": { - "oneofs": { - "kind": { - "oneof": [ - "nullValue", - "numberValue", - "stringValue", - "boolValue", - "structValue", - "listValue" - ] - } - }, - "fields": { - "nullValue": { - "type": "NullValue", - "id": 1 - }, - "numberValue": { - "type": "double", - "id": 2 - }, - "stringValue": { - "type": "string", - "id": 3 - }, - "boolValue": { - "type": "bool", - "id": 4 - }, - "structValue": { - "type": "Struct", - "id": 5 - }, - "listValue": { - "type": "ListValue", - "id": 6 - } - } - }, - "NullValue": { - "values": { - "NULL_VALUE": 0 - } - }, - "ListValue": { - "fields": { - "values": { - "rule": "repeated", - "type": "Value", - "id": 1 - } - } - }, - "Empty": { - "fields": {} - }, "DoubleValue": { "fields": { "value": { @@ -891,9 +1040,12 @@ } } }, + "Empty": { + "fields": {} + }, "Any": { "fields": { - "typeUrl": { + "type_url": { "type": "string", "id": 1 }, @@ -902,6 +1054,18 @@ "id": 2 } } + }, + "Duration": { + "fields": { + "seconds": { + "type": "int64", + "id": 1 + }, + "nanos": { + "type": "int32", + "id": 2 + } + } } } }, @@ -910,9 +1074,9 @@ "v1": { "options": { "csharp_namespace": "Google.Cloud.Firestore.V1", - "go_package": "google.golang.org/genproto/googleapis/firestore/v1;firestore", + "go_package": "cloud.google.com/go/firestore/apiv1/firestorepb;firestorepb", "java_multiple_files": true, - "java_outer_classname": "WriteProto", + "java_outer_classname": "QueryProfileProto", "java_package": "com.google.firestore.v1", "objc_class_prefix": "GCFS", "php_namespace": "Google\\Cloud\\Firestore\\V1", @@ -928,104 +1092,6 @@ } } }, - "BitSequence": { - "fields": { - "bitmap": { - "type": "bytes", - "id": 1 - }, - "padding": { - "type": "int32", - "id": 2 - } - } - }, - "BloomFilter": { - "fields": { - "bits": { - "type": "BitSequence", - "id": 1 - }, - "hashCount": { - "type": "int32", - "id": 2 - } - } - }, - "DocumentMask": { - "fields": { - "fieldPaths": { - "rule": "repeated", - "type": "string", - "id": 1 - } - } - }, - "Precondition": { - "oneofs": { - "conditionType": { - "oneof": [ - "exists", - "updateTime" - ] - } - }, - "fields": { - "exists": { - "type": "bool", - "id": 1 - }, - "updateTime": { - "type": "google.protobuf.Timestamp", - "id": 2 - } - } - }, - "TransactionOptions": { - "oneofs": { - "mode": { - "oneof": [ - "readOnly", - "readWrite" - ] - } - }, - "fields": { - "readOnly": { - "type": "ReadOnly", - "id": 2 - }, - "readWrite": { - "type": "ReadWrite", - "id": 3 - } - }, - "nested": { - "ReadWrite": { - "fields": { - "retryTransaction": { - "type": "bytes", - "id": 1 - } - } - }, - "ReadOnly": { - "oneofs": { - "consistencySelector": { - "oneof": [ - "readTime" - ] - } - }, - "fields": { - "readTime": { - "type": "google.protobuf.Timestamp", - "id": 2 - } - } - } - } - }, "Document": { "fields": { "name": { @@ -1061,7 +1127,10 @@ "referenceValue", "geoPointValue", "arrayValue", - "mapValue" + "mapValue", + "fieldReferenceValue", + "functionValue", + "pipelineValue" ] } }, @@ -1106,27 +1175,184 @@ "type": "ArrayValue", "id": 9 }, - "mapValue": { - "type": "MapValue", - "id": 6 + "mapValue": { + "type": "MapValue", + "id": 6 + }, + "fieldReferenceValue": { + "type": "string", + "id": 19 + }, + "functionValue": { + "type": "Function", + "id": 20 + }, + "pipelineValue": { + "type": "Pipeline", + "id": 21 + } + } + }, + "ArrayValue": { + "fields": { + "values": { + "rule": "repeated", + "type": "Value", + "id": 1 + } + } + }, + "MapValue": { + "fields": { + "fields": { + "keyType": "string", + "type": "Value", + "id": 1 + } + } + }, + "Function": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "args": { + "rule": "repeated", + "type": "Value", + "id": 2 + }, + "options": { + "keyType": "string", + "type": "Value", + "id": 3 + } + } + }, + "Pipeline": { + "fields": { + "stages": { + "rule": "repeated", + "type": "Stage", + "id": 1 + } + }, + "nested": { + "Stage": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "args": { + "rule": "repeated", + "type": "Value", + "id": 2 + }, + "options": { + "keyType": "string", + "type": "Value", + "id": 3 + } + } + } + } + }, + "BitSequence": { + "fields": { + "bitmap": { + "type": "bytes", + "id": 1 + }, + "padding": { + "type": "int32", + "id": 2 + } + } + }, + "BloomFilter": { + "fields": { + "bits": { + "type": "BitSequence", + "id": 1 + }, + "hashCount": { + "type": "int32", + "id": 2 } } }, - "ArrayValue": { + "DocumentMask": { "fields": { - "values": { + "fieldPaths": { "rule": "repeated", - "type": "Value", + "type": "string", "id": 1 } } }, - "MapValue": { + "Precondition": { + "oneofs": { + "conditionType": { + "oneof": [ + "exists", + "updateTime" + ] + } + }, "fields": { - "fields": { - "keyType": "string", - "type": "Value", + "exists": { + "type": "bool", "id": 1 + }, + "updateTime": { + "type": "google.protobuf.Timestamp", + "id": 2 + } + } + }, + "TransactionOptions": { + "oneofs": { + "mode": { + "oneof": [ + "readOnly", + "readWrite" + ] + } + }, + "fields": { + "readOnly": { + "type": "ReadOnly", + "id": 2 + }, + "readWrite": { + "type": "ReadWrite", + "id": 3 + } + }, + "nested": { + "ReadWrite": { + "fields": { + "retryTransaction": { + "type": "bytes", + "id": 1 + } + } + }, + "ReadOnly": { + "oneofs": { + "consistencySelector": { + "oneof": [ + "readTime" + ] + } + }, + "fields": { + "readTime": { + "type": "google.protobuf.Timestamp", + "id": 2 + } + } } } }, @@ -1302,6 +1528,23 @@ } ] }, + "ExecutePipeline": { + "requestType": "ExecutePipelineRequest", + "responseType": "ExecutePipelineResponse", + "responseStream": true, + "options": { + "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:executePipeline", + "(google.api.http).body": "*" + }, + "parsedOptions": [ + { + "(google.api.http)": { + "post": "/v1/{database=projects/*/databases/*}/documents:executePipeline", + "body": "*" + } + } + ] + }, "RunAggregationQuery": { "requestType": "RunAggregationQueryRequest", "responseType": "RunAggregationQueryResponse", @@ -1816,6 +2059,64 @@ } } }, + "ExecutePipelineRequest": { + "oneofs": { + "pipelineType": { + "oneof": [ + "structuredPipeline" + ] + }, + "consistencySelector": { + "oneof": [ + "transaction", + "newTransaction", + "readTime" + ] + } + }, + "fields": { + "database": { + "type": "string", + "id": 1, + "options": { + "(google.api.field_behavior)": "REQUIRED" + } + }, + "structuredPipeline": { + "type": "StructuredPipeline", + "id": 2 + }, + "transaction": { + "type": "bytes", + "id": 5 + }, + "newTransaction": { + "type": "TransactionOptions", + "id": 6 + }, + "readTime": { + "type": "google.protobuf.Timestamp", + "id": 7 + } + } + }, + "ExecutePipelineResponse": { + "fields": { + "transaction": { + "type": "bytes", + "id": 1 + }, + "results": { + "rule": "repeated", + "type": "Document", + "id": 2 + }, + "executionTime": { + "type": "google.protobuf.Timestamp", + "id": 3 + } + } + }, "RunAggregationQueryRequest": { "oneofs": { "queryType": { @@ -2216,6 +2517,19 @@ } } }, + "StructuredPipeline": { + "fields": { + "pipeline": { + "type": "Pipeline", + "id": 1 + }, + "options": { + "keyType": "string", + "type": "Value", + "id": 2 + } + } + }, "StructuredQuery": { "fields": { "select": { @@ -2474,7 +2788,7 @@ "Sum": { "fields": { "field": { - "type": "FieldReference", + "type": "StructuredQuery.FieldReference", "id": 1 } } @@ -2482,7 +2796,7 @@ "Avg": { "fields": { "field": { - "type": "FieldReference", + "type": "StructuredQuery.FieldReference", "id": 1 } } @@ -2694,6 +3008,82 @@ "id": 3 } } + }, + "ExplainOptions": { + "fields": { + "analyze": { + "type": "bool", + "id": 1, + "options": { + "(google.api.field_behavior)": "OPTIONAL" + } + } + } + }, + "ExplainMetrics": { + "fields": { + "planSummary": { + "type": "PlanSummary", + "id": 1 + }, + "executionStats": { + "type": "ExecutionStats", + "id": 2 + } + } + }, + "PlanSummary": { + "fields": { + "indexesUsed": { + "rule": "repeated", + "type": "google.protobuf.Struct", + "id": 1 + } + } + }, + "ExecutionStats": { + "fields": { + "resultsReturned": { + "type": "int64", + "id": 1 + }, + "executionDuration": { + "type": "google.protobuf.Duration", + "id": 3 + }, + "readOperations": { + "type": "int64", + "id": 4 + }, + "debugStats": { + "type": "google.protobuf.Struct", + "id": 5 + } + } + } + } + } + } + }, + "type": { + "options": { + "cc_enable_arenas": true, + "go_package": "google.golang.org/genproto/googleapis/type/latlng;latlng", + "java_multiple_files": true, + "java_outer_classname": "LatLngProto", + "java_package": "com.google.type", + "objc_class_prefix": "GTP" + }, + "nested": { + "LatLng": { + "fields": { + "latitude": { + "type": "double", + "id": 1 + }, + "longitude": { + "type": "double", + "id": 2 } } } @@ -2701,9 +3091,9 @@ }, "api": { "options": { - "go_package": "google.golang.org/genproto/googleapis/api/annotations;annotations", + "go_package": "google.golang.org/genproto/googleapis/api;api", "java_multiple_files": true, - "java_outer_classname": "HttpProto", + "java_outer_classname": "LaunchStageProto", "java_package": "com.google.api", "objc_class_prefix": "GAPI", "cc_enable_arenas": true @@ -2720,6 +3110,10 @@ "rule": "repeated", "type": "HttpRule", "id": 1 + }, + "fullyDecodeReservedExpansion": { + "type": "bool", + "id": 2 } } }, @@ -2737,6 +3131,10 @@ } }, "fields": { + "selector": { + "type": "string", + "id": 1 + }, "get": { "type": "string", "id": 2 @@ -2761,14 +3159,14 @@ "type": "CustomHttpPattern", "id": 8 }, - "selector": { - "type": "string", - "id": 1 - }, "body": { "type": "string", "id": 7 }, + "responseBody": { + "type": "string", + "id": 12 + }, "additionalBindings": { "rule": "repeated", "type": "HttpRule", @@ -2821,29 +3219,17 @@ "UNORDERED_LIST": 6, "NON_EMPTY_DEFAULT": 7 } - } - } - }, - "type": { - "options": { - "cc_enable_arenas": true, - "go_package": "google.golang.org/genproto/googleapis/type/latlng;latlng", - "java_multiple_files": true, - "java_outer_classname": "LatLngProto", - "java_package": "com.google.type", - "objc_class_prefix": "GTP" - }, - "nested": { - "LatLng": { - "fields": { - "latitude": { - "type": "double", - "id": 1 - }, - "longitude": { - "type": "double", - "id": 2 - } + }, + "LaunchStage": { + "values": { + "LAUNCH_STAGE_UNSPECIFIED": 0, + "UNIMPLEMENTED": 6, + "PRELAUNCH": 7, + "EARLY_ACCESS": 1, + "ALPHA": 2, + "BETA": 3, + "GA": 4, + "DEPRECATED": 5 } } } @@ -2880,4 +3266,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index f790ede0d5c..10566503ca3 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -20,10 +20,12 @@ import { User } from '../auth/user'; import { Aggregate } from '../core/aggregate'; import { DatabaseId } from '../core/database_info'; import { queryToAggregateTarget, Query, queryToTarget } from '../core/query'; +import { Pipeline } from '../lite-api/pipeline'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { ResourcePath } from '../model/path'; +import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { ApiClientObjectMap, BatchGetDocumentsRequest as ProtoBatchGetDocumentsRequest, @@ -32,6 +34,8 @@ import { RunAggregationQueryResponse as ProtoRunAggregationQueryResponse, RunQueryRequest as ProtoRunQueryRequest, RunQueryResponse as ProtoRunQueryResponse, + ExecutePipelineRequest as ProtoExecutePipelineRequest, + ExecutePipelineResponse as ProtoExecutePipelineResponse, Value } from '../protos/firestore_proto_api'; import { debugAssert, debugCast, hardAssert } from '../util/assert'; @@ -54,7 +58,9 @@ import { toName, toQueryTarget, toResourcePath, - toRunAggregationQueryRequest + toRunAggregationQueryRequest, + fromPipelineResponse, + getEncodedDatabaseId } from './serializer'; /** @@ -236,6 +242,41 @@ export async function invokeBatchGetDocumentsRpc( return result; } +export async function invokeExecutePipeline( + datastore: Datastore, + pipeline: Pipeline +): Promise { + const datastoreImpl = debugCast(datastore, DatastoreImpl); + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: getEncodedDatabaseId(datastoreImpl.serializer), + structuredPipeline: { + pipeline: pipeline._toProto(datastoreImpl.serializer) + } + }; + + const response = await datastoreImpl.invokeStreamingRPC< + ProtoExecutePipelineRequest, + ProtoExecutePipelineResponse + >( + 'ExecutePipeline', + datastoreImpl.serializer.databaseId, + ResourcePath.emptyPath(), + executePipelineRequest + ); + + return response + .filter(proto => !!proto.results) + .flatMap(proto => { + if (proto.results!.length === 0) { + return fromPipelineResponse(datastoreImpl.serializer, proto); + } else { + return proto.results!.map(result => + fromPipelineResponse(datastoreImpl.serializer, proto, result) + ); + } + }); +} + export async function invokeRunQueryRpc( datastore: Datastore, query: Query diff --git a/packages/firestore/src/remote/internal_serializer.ts b/packages/firestore/src/remote/internal_serializer.ts index 8f278247581..29a68620efc 100644 --- a/packages/firestore/src/remote/internal_serializer.ts +++ b/packages/firestore/src/remote/internal_serializer.ts @@ -19,6 +19,8 @@ import { ensureFirestoreConfigured, Firestore } from '../api/database'; import { AggregateImpl } from '../core/aggregate'; import { queryToAggregateTarget, queryToTarget } from '../core/query'; import { AggregateSpec } from '../lite-api/aggregate_types'; +import { getDatastore } from '../lite-api/components'; +import { Pipeline } from '../lite-api/pipeline'; import { Query } from '../lite-api/reference'; import { cast } from '../util/input_validation'; import { mapToArray } from '../util/obj'; @@ -87,3 +89,28 @@ export function _internalAggregationQueryToProtoRunAggregationQueryRequest< /* skipAliasing= */ true ).request; } + +/** + * @internal + * @private + * + * This function is for internal use only. + * + * Returns the `ExecutePipelineRequest` representation of the given query. + * Returns `null` if the Firestore client associated with the given query has + * not been initialized or has been terminated. + * + * @param pipeline - The Pipeline to convert to proto representation. + */ +export function _internalPipelineToExecutePipelineRequestProto( + pipeline: Pipeline + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const firestore = cast(pipeline._db, Firestore); + const datastore = getDatastore(firestore); + const serializer = datastore.serializer; + if (serializer === undefined) { + return null; + } + return pipeline._toProto(serializer); +} diff --git a/packages/firestore/src/remote/rest_connection.ts b/packages/firestore/src/remote/rest_connection.ts index 2d6889dac3b..7469d8f45ff 100644 --- a/packages/firestore/src/remote/rest_connection.ts +++ b/packages/firestore/src/remote/rest_connection.ts @@ -46,6 +46,7 @@ RPC_NAME_URL_MAPPING['BatchGetDocuments'] = 'batchGet'; RPC_NAME_URL_MAPPING['Commit'] = 'commit'; RPC_NAME_URL_MAPPING['RunQuery'] = 'runQuery'; RPC_NAME_URL_MAPPING['RunAggregationQuery'] = 'runAggregationQuery'; +RPC_NAME_URL_MAPPING['ExecutePipeline'] = 'executePipeline'; const RPC_URL_VERSION = 'v1'; diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 830875f5e1b..d08e0d16874 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { ParseContext } from '../api/parse_context'; import { Aggregate } from '../core/aggregate'; import { Bound } from '../core/bound'; import { DatabaseId } from '../core/database_info'; @@ -37,7 +38,10 @@ import { import { SnapshotVersion } from '../core/snapshot_version'; import { targetIsDocumentTarget, Target } from '../core/target'; import { TargetId } from '../core/types'; +import { Bytes } from '../lite-api/bytes'; +import { GeoPoint } from '../lite-api/geo_point'; import { Timestamp } from '../lite-api/timestamp'; +import { UserDataReader } from '../lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../local/target_data'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -55,6 +59,7 @@ import { import { normalizeTimestamp } from '../model/normalize'; import { ObjectValue } from '../model/object_value'; import { FieldPath, ResourcePath } from '../model/path'; +import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { ArrayRemoveTransformOperation, ArrayUnionTransformOperation, @@ -87,7 +92,11 @@ import { TargetChangeTargetChangeType as ProtoTargetChangeTargetChangeType, Timestamp as ProtoTimestamp, Write as ProtoWrite, - WriteResult as ProtoWriteResult + WriteResult as ProtoWriteResult, + Value as ProtoValue, + MapValue as ProtoMapValue, + ExecutePipelineResponse as ProtoExecutePipelineResponse, + Pipeline } from '../protos/firestore_proto_api'; import { debugAssert, fail, hardAssert } from '../util/assert'; import { ByteString } from '../util/byte_string'; @@ -173,7 +182,7 @@ function fromRpcStatus(status: ProtoStatus): FirestoreError { * our generated proto interfaces say Int32Value must be. But GRPC actually * expects a { value: } struct. */ -function toInt32Proto( +export function toInt32Proto( serializer: JsonProtoSerializer, val: number | null ): number | { value: number } | null { @@ -431,6 +440,37 @@ export function toDocument( }; } +export function fromPipelineResponse( + serializer: JsonProtoSerializer, + proto: ProtoExecutePipelineResponse, + document?: ProtoDocument +): PipelineStreamElement { + const output: PipelineStreamElement = {}; + if (proto.transaction?.length) { + output.transaction = proto.transaction; + } + const executionTime = proto.executionTime + ? fromVersion(proto.executionTime) + : undefined; + output.executionTime = executionTime; + + if (!!document) { + output.key = document.name + ? fromName(serializer, document.name) + : undefined; + + output.fields = new ObjectValue({ mapValue: { fields: document.fields } }); + + output.createTime = document.createTime + ? fromVersion(document.createTime!) + : undefined; + output.updateTime = document.updateTime + ? fromVersion(document.updateTime!) + : undefined; + } + return output; +} + export function fromDocument( serializer: JsonProtoSerializer, document: ProtoDocument, @@ -1414,3 +1454,102 @@ export function isValidResourceName(path: ResourcePath): boolean { path.get(2) === 'databases' ); } + +export interface ProtoSerializable { + _toProto(serializer: JsonProtoSerializer): ProtoType; +} + +export interface ProtoValueSerializable extends ProtoSerializable { + // Supports runtime identification of the ProtoSerializable type. + _protoValueType: 'ProtoValue'; +} + +export function isProtoValueSerializable( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any +): value is ProtoValueSerializable { + return ( + !!value && + typeof value._toProto === 'function' && + value._protoValueType === 'ProtoValue' + ); +} + +export interface UserData { + _readUserData(dataReader: UserDataReader, context?: ParseContext): void; +} + +export function toMapValue( + serializer: JsonProtoSerializer, + input: Map> +): ProtoValue { + const map: ProtoMapValue = { fields: {} }; + input.forEach((exp: ProtoSerializable, key: string) => { + if (typeof key !== 'string') { + throw new Error(`Cannot encode map with non-string key: ${key}`); + } + + map.fields![key] = exp._toProto(serializer)!; + }); + return { + mapValue: map + }; +} + +export function toNullValue(value: null): ProtoValue { + return { nullValue: 'NULL_VALUE' }; +} + +export function toBooleanValue(value: boolean): ProtoValue { + return { booleanValue: value }; +} + +export function toStringValue(value: string): ProtoValue { + return { stringValue: value }; +} + +export function toPipelineValue(value: Pipeline): ProtoValue { + return { pipelineValue: value }; +} + +export function dateToTimestampValue( + serializer: JsonProtoSerializer, + value: Date +): ProtoValue { + const timestamp = Timestamp.fromDate(value); + return { + timestampValue: toTimestamp(serializer, timestamp) + }; +} + +export function timestampToTimestampValue( + serializer: JsonProtoSerializer, + value: Timestamp +): ProtoValue { + // Firestore backend truncates precision down to microseconds. To ensure + // offline mode works the same in regards to truncation, perform the + // truncation immediately without waiting for the backend to do that. + const timestamp = new Timestamp( + value.seconds, + Math.floor(value.nanoseconds / 1000) * 1000 + ); + return { + timestampValue: toTimestamp(serializer, timestamp) + }; +} + +export function toGeoPointValue(value: GeoPoint): ProtoValue { + return { + geoPointValue: { + latitude: value.latitude, + longitude: value.longitude + } + }; +} + +export function toBytesValue( + serializer: JsonProtoSerializer, + value: Bytes +): ProtoValue { + return { bytesValue: toBytes(serializer, value._byteString) }; +} diff --git a/packages/firestore/src/util/misc.ts b/packages/firestore/src/util/misc.ts index 42fa568835b..79fbc27e3f7 100644 --- a/packages/firestore/src/util/misc.ts +++ b/packages/firestore/src/util/misc.ts @@ -151,6 +151,26 @@ export function arrayEquals( } return left.every((value, index) => comparator(value, right[index])); } + +/** + * Verifies equality for an optional value. + */ +export function isOptionalEqual( + left: T | undefined, + right: T | undefined, + equalityTest: (left: T, right: T) => boolean +): boolean { + if (left === undefined && right === undefined) { + return true; + } + + if (left === undefined || right === undefined) { + return false; + } + + return equalityTest(left, right); +} + /** * Returns the immediate lexicographically-following string. This is useful to * construct an inclusive range for indexeddb iterators. diff --git a/packages/firestore/src/util/obj.ts b/packages/firestore/src/util/obj.ts index c40bc86bc5c..2b61da9447f 100644 --- a/packages/firestore/src/util/obj.ts +++ b/packages/firestore/src/util/obj.ts @@ -32,7 +32,7 @@ export function objectSize(obj: object): number { } export function forEach( - obj: Dict | undefined, + obj: Record | undefined, fn: (key: string, val: V) => void ): void { for (const key in obj) { diff --git a/packages/firestore/src/util/proto.ts b/packages/firestore/src/util/proto.ts new file mode 100644 index 00000000000..a99f73cfa9c --- /dev/null +++ b/packages/firestore/src/util/proto.ts @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ArrayValue as ProtoArrayValue, + Function as ProtoFunction, + LatLng as ProtoLatLng, + MapValue as ProtoMapValue, + Pipeline as ProtoPipeline, + Timestamp as ProtoTimestamp, + Value as ProtoValue +} from '../protos/firestore_proto_api'; + +import { isPlainObject } from './input_validation'; + +/* eslint @typescript-eslint/no-explicit-any: 0 */ + +function isITimestamp(obj: any): obj is ProtoTimestamp { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ( + 'seconds' in obj && + (obj.seconds === null || + typeof obj.seconds === 'number' || + typeof obj.seconds === 'string') && + 'nanos' in obj && + (obj.nanos === null || typeof obj.nanos === 'number') + ) { + return true; + } + + return false; +} +function isILatLng(obj: any): obj is ProtoLatLng { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ( + 'latitude' in obj && + (obj.latitude === null || typeof obj.latitude === 'number') && + 'longitude' in obj && + (obj.longitude === null || typeof obj.longitude === 'number') + ) { + return true; + } + + return false; +} +function isIArrayValue(obj: any): obj is ProtoArrayValue { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ('values' in obj && (obj.values === null || Array.isArray(obj.values))) { + return true; + } + + return false; +} +function isIMapValue(obj: any): obj is ProtoMapValue { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ('fields' in obj && (obj.fields === null || isPlainObject(obj.fields))) { + return true; + } + + return false; +} +function isIFunction(obj: any): obj is ProtoFunction { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ( + 'name' in obj && + (obj.name === null || typeof obj.name === 'string') && + 'args' in obj && + (obj.args === null || Array.isArray(obj.args)) + ) { + return true; + } + + return false; +} + +function isIPipeline(obj: any): obj is ProtoPipeline { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + if ('stages' in obj && (obj.stages === null || Array.isArray(obj.stages))) { + return true; + } + + return false; +} + +export function isFirestoreValue(obj: any): obj is ProtoValue { + if (typeof obj !== 'object' || obj === null) { + return false; // Must be a non-null object + } + + // Check optional properties and their types + if ( + ('nullValue' in obj && + (obj.nullValue === null || obj.nullValue === 'NULL_VALUE')) || + ('booleanValue' in obj && + (obj.booleanValue === null || typeof obj.booleanValue === 'boolean')) || + ('integerValue' in obj && + (obj.integerValue === null || + typeof obj.integerValue === 'number' || + typeof obj.integerValue === 'string')) || + ('doubleValue' in obj && + (obj.doubleValue === null || typeof obj.doubleValue === 'number')) || + ('timestampValue' in obj && + (obj.timestampValue === null || isITimestamp(obj.timestampValue))) || + ('stringValue' in obj && + (obj.stringValue === null || typeof obj.stringValue === 'string')) || + ('bytesValue' in obj && + (obj.bytesValue === null || obj.bytesValue instanceof Uint8Array)) || + ('referenceValue' in obj && + (obj.referenceValue === null || + typeof obj.referenceValue === 'string')) || + ('geoPointValue' in obj && + (obj.geoPointValue === null || isILatLng(obj.geoPointValue))) || + ('arrayValue' in obj && + (obj.arrayValue === null || isIArrayValue(obj.arrayValue))) || + ('mapValue' in obj && + (obj.mapValue === null || isIMapValue(obj.mapValue))) || + ('fieldReferenceValue' in obj && + (obj.fieldReferenceValue === null || + typeof obj.fieldReferenceValue === 'string')) || + ('functionValue' in obj && + (obj.functionValue === null || isIFunction(obj.functionValue))) || + ('pipelineValue' in obj && + (obj.pipelineValue === null || isIPipeline(obj.pipelineValue))) + ) { + return true; + } + + return false; +} diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index c298bfe2131..361ebda1935 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -51,6 +51,10 @@ export function isSafeInteger(value: unknown): boolean { ); } +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + /** The subset of the browser's Window interface used by the SDK. */ export interface WindowLike { readonly localStorage: Storage; diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts new file mode 100644 index 00000000000..fb04f775972 --- /dev/null +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -0,0 +1,3077 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { + AggregateFunction, + ascending, + BooleanExpr, + byteLength, + constantVector, + FunctionExpr, + timestampAdd, + timestampToUnixMicros, + timestampToUnixMillis, + timestampToUnixSeconds, + toLower, + unixMicrosToTimestamp, + unixMillisToTimestamp, + vectorLength +} from '../../../src/lite-api/expressions'; +import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; +import { addEqualityMatcher } from '../../util/equality_matcher'; +import { Deferred } from '../../util/promise'; +import { + GeoPoint, + Timestamp, + Bytes, + getFirestore, + terminate, + vector, + CollectionReference, + doc, + DocumentData, + Firestore, + setDoc, + setLogLevel, + collection, + documentId as documentIdFieldPath, + writeBatch, + addDoc +} from '../util/firebase_export'; +import { apiDescribe, withTestCollection, itIf } from '../util/helpers'; +import { + array, + mod, + pipelineResultEqual, + sum, + replaceFirst, + replaceAll, + descending, + isNan, + map, + execute, + add, + arrayContainsAll, + unixSecondsToTimestamp, + and, + arrayContains, + arrayContainsAny, + count, + avg, + cosineDistance, + not, + countAll, + dotProduct, + endsWith, + eq, + reverse, + toUpper, + euclideanDistance, + gt, + like, + lt, + strContains, + divide, + lte, + arrayLength, + arrayConcat, + mapGet, + neq, + or, + regexContains, + regexMatch, + startsWith, + strConcat, + subtract, + cond, + eqAny, + logicalMaximum, + notEqAny, + multiply, + countIf, + bitAnd, + bitOr, + bitXor, + bitNot, + exists, + bitLeftShift, + charLength, + bitRightShift, + rand, + arrayOffset, + minimum, + maximum, + isError, + ifError, + trim, + isAbsent, + isNull, + isNotNull, + isNotNan, + timestampSub, + mapRemove, + mapMerge, + documentId, + substr, + logicalMinimum, + xor, + field, + constant, + _internalPipelineToExecutePipelineRequestProto, + FindNearestOptions +} from '../util/pipeline_export'; + +use(chaiAsPromised); + +setLogLevel('debug'); + +const testUnsupportedFeatures: boolean | 'only' = false; +const timestampDeltaMS = 1000; + +apiDescribe('Pipelines', persistence => { + addEqualityMatcher(); + + let firestore: Firestore; + let randomCol: CollectionReference; + let beginDocCreation: number = 0; + let endDocCreation: number = 0; + + async function testCollectionWithDocs(docs: { + [id: string]: DocumentData; + }): Promise> { + beginDocCreation = new Date().valueOf(); + for (const id in docs) { + if (docs.hasOwnProperty(id)) { + const ref = doc(randomCol, id); + await setDoc(ref, docs[id]); + } + } + endDocCreation = new Date().valueOf(); + return randomCol; + } + + function expectResults(snapshot: PipelineSnapshot, ...docs: string[]): void; + function expectResults( + snapshot: PipelineSnapshot, + ...data: DocumentData[] + ): void; + + function expectResults( + snapshot: PipelineSnapshot, + ...data: DocumentData[] | string[] + ): void { + const docs = snapshot.results; + + expect(docs.length).to.equal(data.length); + + if (data.length > 0) { + if (typeof data[0] === 'string') { + const actualIds = docs.map(doc => doc.id); + expect(actualIds).to.deep.equal(data); + } else { + docs.forEach(r => { + expect(r.data()).to.deep.equal(data.shift()); + }); + } + } + } + + async function setupBookDocs(): Promise> { + const bookDocs: { [id: string]: DocumentData } = { + book1: { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + embedding: vector([10, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + }, + book2: { + title: 'Pride and Prejudice', + author: 'Jane Austen', + genre: 'Romance', + published: 1813, + rating: 4.5, + tags: ['classic', 'social commentary', 'love'], + awards: { none: true }, + embedding: vector([1, 10, 1, 1, 1, 1, 1, 1, 1, 1]) + }, + book3: { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + genre: 'Magical Realism', + published: 1967, + rating: 4.3, + tags: ['family', 'history', 'fantasy'], + awards: { nobel: true, nebula: false }, + embedding: vector([1, 1, 10, 1, 1, 1, 1, 1, 1, 1]) + }, + book4: { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + genre: 'Fantasy', + published: 1954, + rating: 4.7, + tags: ['adventure', 'magic', 'epic'], + awards: { hugo: false, nebula: false }, + remarks: null, + cost: NaN, + embedding: vector([1, 1, 1, 10, 1, 1, 1, 1, 1, 1]) + }, + book5: { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + genre: 'Dystopian', + published: 1985, + rating: 4.1, + tags: ['feminism', 'totalitarianism', 'resistance'], + awards: { 'arthur c. clarke': true, 'booker prize': false }, + embedding: vector([1, 1, 1, 1, 10, 1, 1, 1, 1, 1]) + }, + book6: { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + genre: 'Psychological Thriller', + published: 1866, + rating: 4.3, + tags: ['philosophy', 'crime', 'redemption'], + awards: { none: true }, + embedding: vector([1, 1, 1, 1, 1, 10, 1, 1, 1, 1]) + }, + book7: { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + genre: 'Southern Gothic', + published: 1960, + rating: 4.2, + tags: ['racism', 'injustice', 'coming-of-age'], + awards: { pulitzer: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 10, 1, 1, 1]) + }, + book8: { + title: '1984', + author: 'George Orwell', + genre: 'Dystopian', + published: 1949, + rating: 4.2, + tags: ['surveillance', 'totalitarianism', 'propaganda'], + awards: { prometheus: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 10, 1, 1]) + }, + book9: { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + genre: 'Modernist', + published: 1925, + rating: 4.0, + tags: ['wealth', 'american dream', 'love'], + awards: { none: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 1, 10, 1]) + }, + book10: { + title: 'Dune', + author: 'Frank Herbert', + genre: 'Science Fiction', + published: 1965, + rating: 4.6, + tags: ['politics', 'desert', 'ecology'], + awards: { hugo: true, nebula: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 1, 1, 10]) + } + }; + return testCollectionWithDocs(bookDocs); + } + + let testDeferred: Deferred | undefined; + let withTestCollectionPromise: Promise | undefined; + + beforeEach(async () => { + const setupDeferred = new Deferred(); + testDeferred = new Deferred(); + withTestCollectionPromise = withTestCollection( + persistence, + {}, + async (collectionRef, firestoreInstance) => { + randomCol = collectionRef; + firestore = firestoreInstance; + await setupBookDocs(); + setupDeferred.resolve(); + + return testDeferred?.promise; + } + ); + + await setupDeferred.promise; + }); + + afterEach(async () => { + testDeferred?.resolve(); + await withTestCollectionPromise; + }); + + describe('pipeline results', () => { + it('empty snapshot as expected', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol.path).limit(0) + ); + expect(snapshot.results.length).to.equal(0); + }); + + it('full snapshot as expected', async () => { + const ppl = firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('__name__')); + const snapshot = await execute(ppl); + expect(snapshot.results.length).to.equal(10); + expect(snapshot.pipeline).to.equal(ppl); + expectResults( + snapshot, + 'book1', + 'book10', + 'book2', + 'book3', + 'book4', + 'book5', + 'book6', + 'book7', + 'book8', + 'book9' + ); + }); + + it('result equals works', async () => { + const ppl = firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .limit(1); + const snapshot1 = await execute(ppl); + const snapshot2 = await execute(ppl); + expect(snapshot1.results.length).to.equal(1); + expect(snapshot2.results.length).to.equal(1); + expect(pipelineResultEqual(snapshot1.results[0], snapshot2.results[0])).to + .be.true; + }); + + it('returns execution time', async () => { + const start = new Date().valueOf(); + const pipeline = firestore.pipeline().collection(randomCol.path); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it('returns execution time for an empty query', async () => { + const start = new Date().valueOf(); + const pipeline = firestore.pipeline().collection(randomCol.path).limit(0); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.results.length).to.equal(0); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it('returns create and update time for each document', async () => { + const pipeline = firestore.pipeline().collection(randomCol.path); + + let snapshot = await execute(pipeline); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(doc => { + expect(doc.createTime).to.not.be.null; + expect(doc.updateTime).to.not.be.null; + + expect(doc.createTime!.toDate().valueOf()).to.approximately( + (beginDocCreation + endDocCreation) / 2, + timestampDeltaMS + ); + expect(doc.updateTime!.toDate().valueOf()).to.approximately( + (beginDocCreation + endDocCreation) / 2, + timestampDeltaMS + ); + expect(doc.createTime?.valueOf()).to.equal(doc.updateTime?.valueOf()); + }); + + const wb = writeBatch(firestore); + snapshot.results.forEach(doc => { + wb.update(doc.ref!, { newField: 'value' }); + }); + await wb.commit(); + + snapshot = await execute(pipeline); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(doc => { + expect(doc.createTime).to.not.be.null; + expect(doc.updateTime).to.not.be.null; + expect(doc.createTime!.toDate().valueOf()).to.be.lessThan( + doc.updateTime!.toDate().valueOf() + ); + }); + }); + + it('returns execution time for an aggregate query', async () => { + const start = new Date().valueOf(); + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate(avg('rating').as('avgRating')); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.results.length).to.equal(1); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it('returns undefined create and update time for each result in an aggregate query', async () => { + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [avg('rating').as('avgRating')], + groups: ['genre'] + }); + + const snapshot = await execute(pipeline); + + expect(snapshot.results.length).to.equal(8); + + snapshot.results.forEach(doc => { + expect(doc.updateTime).to.be.undefined; + expect(doc.createTime).to.be.undefined; + }); + }); + }); + + describe('pipeline sources', () => { + it('supports CollectionReference as source', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol) + ); + expect(snapshot.results.length).to.equal(10); + }); + + it('supports list of documents as source', async () => { + const collName = randomCol.id; + + const snapshot = await execute( + firestore + .pipeline() + .documents([ + `${collName}/book1`, + doc(randomCol, 'book2'), + doc(randomCol, 'book3').path + ]) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('reject CollectionReference for another DB', async () => { + const db2 = getFirestore(firestore.app, 'notDefault'); + + expect(() => { + firestore.pipeline().collection(collection(db2, 'foo')); + }).to.throw(/Invalid CollectionReference/); + + await terminate(db2); + }); + + it('reject DocumentReference for another DB', async () => { + const db2 = getFirestore(firestore.app, 'notDefault'); + + expect(() => { + firestore.pipeline().documents([doc(db2, 'foo/bar')]); + }).to.throw(/Invalid DocumentReference/); + + await terminate(db2); + }); + + // Subcollections not currently supported in DBE + itIf(testUnsupportedFeatures)( + 'supports collection group as source', + async () => { + const randomSubCollectionId = Math.random().toString(16).slice(2); + const doc1 = await addDoc( + collection(randomCol, 'book1', randomSubCollectionId), + { order: 1 } + ); + const doc2 = await addDoc( + collection(randomCol, 'book2', randomSubCollectionId), + { order: 2 } + ); + const snapshot = await execute( + firestore + .pipeline() + .collectionGroup(randomSubCollectionId) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + } + ); + + // subcollections not currently supported in dbe + itIf(testUnsupportedFeatures)('supports database as source', async () => { + const randomId = Math.random().toString(16).slice(2); + const doc1 = await addDoc(collection(randomCol, 'book1', 'sub'), { + order: 1, + randomId + }); + const doc2 = await addDoc(collection(randomCol, 'book2', 'sub'), { + order: 2, + randomId + }); + const snapshot = await execute( + firestore + .pipeline() + .database() + .where(eq('randomId', randomId)) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + }); + }); + + describe('supported data types', () => { + it('accepts and returns all data types', async () => { + const refDate = new Date(); + const refTimestamp = Timestamp.now(); + const constants = [ + constant(1).as('number'), + constant('a string').as('string'), + constant(true).as('boolean'), + constant(null).as('null'), + constant(new GeoPoint(0.1, 0.2)).as('geoPoint'), + constant(refTimestamp).as('timestamp'), + constant(refDate).as('date'), + constant( + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) + ).as('bytes'), + constant(doc(firestore, 'foo', 'bar')).as('documentReference'), + constantVector(vector([1, 2, 3])).as('vectorValue'), + constantVector([1, 2, 3]).as('vectorValue2'), + map({ + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': refDate, + 'uint8Array': Bytes.fromUint8Array( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0]) + ), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'map': { + 'number': 2, + 'string': 'b string' + }, + 'array': [1, 'c string'] + }).as('map'), + array([ + 1, + 'a string', + true, + null, + new GeoPoint(0.1, 0.2), + refTimestamp, + refDate, + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + doc(firestore, 'foo', 'bar'), + vector([1, 2, 3]), + { + 'number': 2, + 'string': 'b string' + } + ]).as('array') + ]; + + const snapshots = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constants[0], ...constants.slice(1)) + ); + + expectResults(snapshots, { + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': Timestamp.fromDate(refDate), + 'bytes': Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'vectorValue2': vector([1, 2, 3]), + 'map': { + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': Timestamp.fromDate(refDate), + 'uint8Array': Bytes.fromUint8Array( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0]) + ), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'map': { + 'number': 2, + 'string': 'b string' + }, + 'array': [1, 'c string'] + }, + 'array': [ + 1, + 'a string', + true, + null, + new GeoPoint(0.1, 0.2), + refTimestamp, + Timestamp.fromDate(refDate), + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + doc(firestore, 'foo', 'bar'), + vector([1, 2, 3]), + { + 'number': 2, + 'string': 'b string' + } + ] + }); + }); + + it('throws on undefined in a map', async () => { + expect(() => { + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + map({ + 'number': 1, + undefined + }).as('foo') + ); + }).to.throw( + 'Function map() called with invalid data. Unsupported field value: undefined' + ); + }); + + it('throws on undefined in an array', async () => { + expect(() => { + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(array([1, undefined]).as('foo')); + }).to.throw( + 'Function array() called with invalid data. Unsupported field value: undefined' + ); + }); + + it('converts arrays and plain objects to functionValues if the customer intent is unspecified', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + 'title', + 'author', + 'genre', + 'rating', + 'published', + 'tags', + 'awards' + ) + .addFields( + array([ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]).as('metadataArray'), + map({ + genre: field('genre'), + rating: multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: { + published: field('published') + } + }).as('metadata') + ) + .where( + and( + eq('metadataArray', [ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]), + eq('metadata', { + genre: field('genre'), + rating: multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: { + published: field('published') + } + }) + ) + ) + ); + + expect(snapshot.results.length).to.equal(1); + + expectResults(snapshot, { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + genre: 'Fantasy', + published: 1954, + rating: 4.7, + tags: ['adventure', 'magic', 'epic'], + awards: { hugo: false, nebula: false }, + metadataArray: [ + 1, + 2, + 'Fantasy', + 47, + ['The Lord of the Rings'], + { + published: 1954 + } + ], + metadata: { + genre: 'Fantasy', + rating: 47, + nestedArray: ['The Lord of the Rings'], + nestedMap: { + published: 1954 + } + } + }); + }); + }); + + describe('stages', () => { + describe('aggregate stage', () => { + it('supports aggregate', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countAll().as('count')) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + avg('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + ) + ); + expectResults(snapshot, { + count: 2, + avgRating: 4.4, + maxRating: 4.6, + sumRating: 8.8 + }); + }); + + it('rejects groups without accumulators', async () => { + await expect( + execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(lt('published', 1900)) + .aggregate({ + accumulators: [], + groups: ['genre'] + }) + ) + ).to.be.rejected; + }); + + it('returns group and accumulate results', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(lt(field('published'), 1984)) + .aggregate({ + accumulators: [avg('rating').as('avgRating')], + groups: ['genre'] + }) + .where(gt('avgRating', 4.3)) + .sort(field('avgRating').descending()) + ); + expectResults( + snapshot, + { avgRating: 4.7, genre: 'Fantasy' }, + { avgRating: 4.5, genre: 'Romance' }, + { avgRating: 4.4, genre: 'Science Fiction' } + ); + }); + + it('returns min, max, count, and countAll accumulations', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + count('cost').as('booksWithCost'), + countAll().as('count'), + maximum('rating').as('maxRating'), + minimum('published').as('minPublished') + ) + ); + expectResults(snapshot, { + booksWithCost: 1, + count: 10, + maxRating: 4.7, + minPublished: 1813 + }); + }); + + it('returns countif accumulation', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countIf(field('rating').gt(4.3)).as('count')) + ); + const expectedResults = { + count: 3 + }; + expectResults(snapshot, expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(field('rating').gt(4.3).countIf().as('count')) + ); + expectResults(snapshot, expectedResults); + }); + }); + + describe('distinct stage', () => { + it('returns distinct values as expected', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort(field('genre').ascending(), field('author').ascending()) + ); + expectResults( + snapshot, + { genre: 'Dystopian', author: 'George Orwell' }, + { genre: 'Dystopian', author: 'Margaret Atwood' }, + { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, + { genre: 'Magical Realism', author: 'Gabriel García Márquez' }, + { genre: 'Modernist', author: 'F. Scott Fitzgerald' }, + { genre: 'Psychological Thriller', author: 'Fyodor Dostoevsky' }, + { genre: 'Romance', author: 'Jane Austen' }, + { genre: 'Science Fiction', author: 'Douglas Adams' }, + { genre: 'Science Fiction', author: 'Frank Herbert' }, + { genre: 'Southern Gothic', author: 'Harper Lee' } + ); + }); + }); + + describe('select stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }, + { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, + { title: 'Dune', author: 'Frank Herbert' }, + { title: 'Crime and Punishment', author: 'Fyodor Dostoevsky' }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez' + }, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' }, + { title: 'Pride and Prejudice', author: 'Jane Austen' }, + { title: "The Handmaid's Tale", author: 'Margaret Atwood' } + ); + }); + }); + + describe('addField stage', () => { + it('can add fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields(constant('bar').as('foo')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + foo: 'bar' + }, + { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + foo: 'bar' + }, + { title: 'Dune', author: 'Frank Herbert', foo: 'bar' }, + { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + foo: 'bar' + }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + foo: 'bar' + }, + { title: '1984', author: 'George Orwell', foo: 'bar' }, + { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + foo: 'bar' + }, + { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + foo: 'bar' + }, + { title: 'Pride and Prejudice', author: 'Jane Austen', foo: 'bar' }, + { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + foo: 'bar' + } + ); + }); + }); + + describe('removeFields stage', () => { + it('can remove fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + .removeFields(field('author')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + }); + + describe('where stage', () => { + it('where with and (2 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.5), + eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + ) + ); + expectResults(snapshot, 'book10', 'book4'); + }); + it('where with and (3 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.5), + eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lt('published', 1965) + ) + ) + ); + expectResults(snapshot, 'book4'); + }); + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy') + ) + ) + .sort(ascending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: '1984' }, + { title: 'Pride and Prejudice' }, + { title: "The Handmaid's Tale" }, + { title: 'The Lord of the Rings' } + ); + }); + + it('where with xor', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + xor( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy'), + eq('published', 1949) + ) + ) + .select('title') + ); + expectResults( + snapshot, + { title: 'Pride and Prejudice' }, + { title: 'The Lord of the Rings' }, + { title: "The Handmaid's Tale" } + ); + }); + + // it('where with boolean constant', async () => { + // const snapshot = await execute( + // firestore + // .pipeline() + // .collection(randomCol.path) + // .where(constant(true)) + // .sort(ascending('__name__')) + // .limit(2) + // ); + // expectResults(snapshot, 'book1', 'book10'); + // }); + }); + + describe('sort, offset, and limit stages', () => { + it('supports sort, offset, and limits', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('author').ascending()) + .offset(5) + .limit(3) + .select('title', 'author') + ); + expectResults( + snapshot, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } + ); + }); + }); + + describe('generic stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .genericStage('select', [ + { + title: field('title'), + metadata: { + 'author': field('author') + } + } + ]) + .sort(field('author').ascending()) + .limit(1) + ); + expectResults(snapshot, { + metadata: { + author: 'Frank Herbert' + }, + title: 'Dune' + }); + }); + + it('can add fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('author').ascending()) + .limit(1) + .select('title', 'author') + .genericStage('add_fields', [ + { + display: strConcat('title', ' - ', field('author')) + } + ]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + display: "The Hitchhiker's Guide to the Galaxy - Douglas Adams" + }); + }); + + it('can filter with where', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('where', [field('author').eq('Douglas Adams')]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }); + }); + + it('can limit, offset, and sort', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('sort', [ + { + direction: 'ascending', + expression: field('author') + } + ]) + .genericStage('offset', [3]) + .genericStage('limit', [1]) + ); + expectResults(snapshot, { + author: 'Fyodor Dostoevsky', + title: 'Crime and Punishment' + }); + }); + + it('can perform aggregate query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('aggregate', [ + { averageRating: field('rating').avg() }, + {} + ]) + ); + expectResults(snapshot, { + averageRating: 4.3100000000000005 + }); + }); + + it('can perform distinct query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('distinct', [{ rating: field('rating') }]) + .sort(field('rating').descending()) + ); + expectResults( + snapshot, + { + rating: 4.7 + }, + { + rating: 4.6 + }, + { + rating: 4.5 + }, + { + rating: 4.3 + }, + { + rating: 4.2 + }, + { + rating: 4.1 + }, + { + rating: 4.0 + } + ); + }); + }); + + describe('replaceWith stage', () => { + it('run pipeline with replaceWith field name', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith('awards') + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }); + }); + + it('run pipeline with replaceWith Expr result', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith( + map({ + foo: 'bar', + baz: { + title: field('title') + } + }) + ) + ); + expectResults(snapshot, { + foo: 'bar', + baz: { title: "The Hitchhiker's Guide to the Galaxy" } + }); + }); + }); + + describe('sample stage', () => { + it('run pipeline with sample limit of 3', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol.path).sample(3) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {documents: 3}', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ documents: 3 }) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {percentage: 0.6}', async () => { + let avgSize = 0; + const numIterations = 20; + for (let i = 0; i < numIterations; i++) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ percentage: 0.6 }) + ); + + avgSize += snapshot.results.length; + } + avgSize /= numIterations; + expect(avgSize).to.be.closeTo(6, 1); + }); + }); + + describe('union stage', () => { + it('run pipeline with union', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union(firestore.pipeline().collection(randomCol.path)) + .sort(field(documentIdFieldPath()).ascending()) + ); + expectResults( + snapshot, + 'book1', + 'book1', + 'book10', + 'book10', + 'book2', + 'book2', + 'book3', + 'book3', + 'book4', + 'book4', + 'book5', + 'book5', + 'book6', + 'book6', + 'book7', + 'book7', + 'book8', + 'book8', + 'book9', + 'book9' + ); + }); + }); + + describe('unnest stage', () => { + it('run pipeline with unnest', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag'), 'tagsIndex') + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + it('unnest an expr', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(array([1, 2, 3]).as('copy')) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'copy', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 1, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 2, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 3, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + }); + + describe('findNearest stage', () => { + it('run pipeline with findNearest', async () => { + const measures: Array = [ + 'euclidean', + 'dot_product', + 'cosine' + ]; + for (const measure of measures) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), + limit: 3, + distanceMeasure: measure + }) + .select('title') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'One Hundred Years of Solitude' + }, + { + title: "The Handmaid's Tale" + } + ); + } + }); + + it('optionally returns the computed distance', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + limit: 2, + distanceMeasure: 'euclidean', + distanceField: 'computedDistance' + }) + .select('title', 'computedDistance') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + computedDistance: 1 + }, + { + title: 'One Hundred Years of Solitude', + computedDistance: 12.041594578792296 + } + ); + }); + }); + }); + + describe('function expressions', () => { + it('logical max works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMaximum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1961 }, + { title: 'Crime and Punishment', 'published-safe': 1961 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); + + it('logical min works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMinimum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1949 }, + { title: 'Crime and Punishment', 'published-safe': 1866 }, + { title: 'Dune', 'published-safe': 1960 } + ); + }); + + it('cond works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + cond( + lt(field('published'), 1960), + constant(1960), + field('published') + ).as('published-safe') + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1960 }, + { title: 'Crime and Punishment', 'published-safe': 1960 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); + + it('eqAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eqAny('published', [1979, 1999, 1967])) + .sort(descending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'One Hundred Years of Solitude' } + ); + }); + + it('notEqAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + notEqAny( + 'published', + [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] + ) + ) + .select('title') + ); + expectResults(snapshot, { title: 'Pride and Prejudice' }); + }); + + it('arrayContains works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContains('tags', 'comedy')) + .select('title') + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('arrayContainsAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContainsAny('tags', ['comedy', 'classic'])) + .sort(descending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'Pride and Prejudice' } + ); + }); + + it('arrayContainsAll works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContainsAll('tags', ['adventure', 'magic'])) + .select('title') + ); + expectResults(snapshot, { title: 'The Lord of the Rings' }); + }); + + it('arrayLength works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(arrayLength('tags').as('tagsCount')) + .where(eq('tagsCount', 3)) + ); + expect(snapshot.results.length).to.equal(10); + }); + + it('testStrConcat', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('author')) + .select( + field('author').strConcat(' - ', field('title')).as('bookInfo') + ) + .limit(1) + ); + expectResults(snapshot, { + bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('testStartsWith', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(startsWith('title', 'The')) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: 'The Great Gatsby' }, + { title: "The Handmaid's Tale" }, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'The Lord of the Rings' } + ); + }); + + it('testEndsWith', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(endsWith('title', 'y')) + .select('title') + .sort(field('title').descending()) + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'The Great Gatsby' } + ); + }); + + it('testStrContains', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(strContains('title', "'s")) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: "The Handmaid's Tale" }, + { title: "The Hitchhiker's Guide to the Galaxy" } + ); + }); + + it('testLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(charLength('title').as('titleLength'), field('title')) + .where(gt('titleLength', 20)) + .sort(field('title').ascending()) + ); + + expectResults( + snapshot, + + { + titleLength: 29, + title: 'One Hundred Years of Solitude' + }, + { + titleLength: 36, + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + titleLength: 21, + title: 'The Lord of the Rings' + }, + { + titleLength: 21, + title: 'To Kill a Mockingbird' + } + ); + }); + + it('testLike', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(like('title', '%Guide%')) + .select('title') + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('testRegexContains', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(regexContains('title', '(?i)(the|of)')) + ); + expect(snapshot.results.length).to.equal(5); + }); + + it('testRegexMatches', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(regexMatch('title', '.*(?i)(the|of).*')) + ); + expect(snapshot.results.length).to.equal(5); + }); + + it('testArithmeticOperations', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'To Kill a Mockingbird')) + .select( + add(field('rating'), 1).as('ratingPlusOne'), + subtract(field('published'), 1900).as('yearsSince1900'), + field('rating').multiply(10).as('ratingTimesTen'), + divide('rating', 2).as('ratingDividedByTwo'), + multiply('rating', 10, 2).as('ratingTimes20'), + add('rating', 1, 2).as('ratingPlus3'), + mod('rating', 2).as('ratingMod2') + ) + .limit(1) + ); + expectResults(snapshot, { + ratingPlusOne: 5.2, + yearsSince1900: 60, + ratingTimesTen: 42, + ratingDividedByTwo: 2.1, + ratingTimes20: 84, + ratingPlus3: 7.2, + ratingMod2: 0.20000000000000018 + }); + }); + + it('testComparisonOperators', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.2), + lte(field('rating'), 4.5), + neq('genre', 'Science Fiction') + ) + ) + .select('rating', 'title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { rating: 4.3, title: 'Crime and Punishment' }, + { + rating: 4.3, + title: 'One Hundred Years of Solitude' + }, + { rating: 4.5, title: 'Pride and Prejudice' } + ); + }); + + it('testLogicalOperators', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + and(gt('rating', 4.5), eq('genre', 'Science Fiction')), + lt('published', 1900) + ) + ) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: 'Crime and Punishment' }, + { title: 'Dune' }, + { title: 'Pride and Prejudice' } + ); + }); + + it('testChecks', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + isNull('rating').as('ratingIsNull'), + isNan('rating').as('ratingIsNaN'), + isError(arrayOffset('title', 0)).as('isError'), + ifError(arrayOffset('title', 0), constant('was error')).as( + 'ifError' + ), + isAbsent('foo').as('isAbsent'), + isNotNull('title').as('titleIsNotNull'), + isNotNan('cost').as('costIsNotNan'), + exists('fooBarBaz').as('fooBarBazExists'), + field('title').exists().as('titleExists') + ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false, + fooBarBazExists: false, + titleExists: true + }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + field('rating').isNull().as('ratingIsNull'), + field('rating').isNan().as('ratingIsNaN'), + arrayOffset('title', 0).isError().as('isError'), + arrayOffset('title', 0) + .ifError(constant('was error')) + .as('ifError'), + field('foo').isAbsent().as('isAbsent'), + field('title').isNotNull().as('titleIsNotNull'), + field('cost').isNotNan().as('costIsNotNan') + ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false + }); + }); + + it('testMapGet', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('published').descending()) + .select( + field('awards').mapGet('hugo').as('hugoAward'), + field('awards').mapGet('others').as('others'), + field('title') + ) + .where(eq('hugoAward', true)) + ); + expectResults( + snapshot, + { + hugoAward: true, + title: "The Hitchhiker's Guide to the Galaxy", + others: { unknown: { year: 1980 } } + }, + { hugoAward: true, title: 'Dune', others: null } + ); + }); + + it('testDistanceFunctions', async () => { + const sourceVector = [0.1, 0.1]; + const targetVector = [0.5, 0.8]; + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + cosineDistance(constantVector(sourceVector), targetVector).as( + 'cosineDistance' + ), + dotProduct(constantVector(sourceVector), targetVector).as( + 'dotProductDistance' + ), + euclideanDistance(constantVector(sourceVector), targetVector).as( + 'euclideanDistance' + ) + ) + .limit(1) + ); + + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855 + }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + constantVector(sourceVector) + .cosineDistance(targetVector) + .as('cosineDistance'), + constantVector(sourceVector) + .dotProduct(targetVector) + .as('dotProductDistance'), + constantVector(sourceVector) + .euclideanDistance(targetVector) + .as('euclideanDistance') + ) + .limit(1) + ); + + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855 + }); + }); + + it('testVectorLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + ); + expectResults(snapshot, { + vectorLength: 3 + }); + }); + + it('testNestedFields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('awards.hugo', true)) + .sort(descending('title')) + .select('title', 'awards.hugo') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + 'awards.hugo': true + }, + { title: 'Dune', 'awards.hugo': true } + ); + }); + + it('test mapGet with field name including . notation', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + ) + .sort(descending('title')) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + 'nestedField.level.`1`': null, + nested: true + }, + { title: 'Dune', 'nestedField.level.`1`': null, nested: null } + ); + }); + + describe('genericFunction', () => { + it('add selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(descending('rating')) + .limit(1) + .select( + new FunctionExpr('add', [field('rating'), constant(1)]).as( + 'rating' + ) + ) + ); + expectResults(snapshot, { + rating: 5.7 + }); + }); + + it('and (variadic) selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('and', [ + field('rating').gt(0), + field('title').charLength().lt(5), + field('tags').arrayContains('propaganda') + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: '1984' + }); + }); + + it('array contains any', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('array_contains_any', [ + field('tags'), + array(['politics']) + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: 'Dune' + }); + }); + + it('countif aggregate', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( + 'countOfBest' + ) + ) + ); + expectResults(snapshot, { + countOfBest: 3 + }); + }); + + it('sort by char_len', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort( + new FunctionExpr('char_length', [field('title')]).ascending(), + descending('__name__') + ) + .limit(3) + .select('title') + ); + expectResults( + snapshot, + { + title: '1984' + }, + { + title: 'Dune' + }, + { + title: 'The Great Gatsby' + } + ); + }); + }); + + itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'The Lord of the Rings')) + .limit(1) + .select(replaceFirst('title', 'o', '0').as('newName')) + ); + expectResults(snapshot, { newName: 'The L0rd of the Rings' }); + }); + + itIf(testUnsupportedFeatures)('testReplaceAll', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'The Lord of the Rings')) + .limit(1) + .select(replaceAll('title', 'o', '0').as('newName')) + ); + expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); + }); + + it('supports Rand', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(10) + .select(rand().as('result')) + ); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(d => { + expect(d.get('result')).to.be.lt(1); + expect(d.get('result')).to.be.gte(0); + }); + }); + + it('supports array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(array([1, 2, 3, 4]).as('metadata')) + ); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: [1, 2, 3, 4] + }); + }); + + it('evaluates expression in array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + array([1, 2, field('genre'), multiply('rating', 10)]).as('metadata') + ) + ); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: [1, 2, 'Fantasy', 47] + }); + }); + + it('supports arrayOffset', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(arrayOffset('tags', 0).as('firstTag')) + ); + const expectedResults = [ + { + firstTag: 'adventure' + }, + { + firstTag: 'politics' + }, + { + firstTag: 'classic' + } + ]; + expectResults(snapshot, ...expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(field('tags').arrayOffset(0).as('firstTag')) + ); + expectResults(snapshot, ...expectedResults); + }); + + it('supports map', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + map({ + foo: 'bar' + }).as('metadata') + ) + ); + + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + foo: 'bar' + } + }); + }); + + it('evaluates expression in map', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + map({ + genre: field('genre'), + rating: field('rating').multiply(10) + }).as('metadata') + ) + ); + + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + genre: 'Fantasy', + rating: 47 + } + }); + }); + + it('supports mapRemove', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(mapRemove('awards', 'hugo').as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false } + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('awards').mapRemove('hugo').as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false } + }); + }); + + it('supports mapMerge', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(mapMerge('awards', { fakeAward: true }).as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false, hugo: false, fakeAward: true } + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('awards').mapMerge({ fakeAward: true }).as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false, hugo: false, fakeAward: true } + }); + }); + + it('supports timestamp conversions', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + unixSecondsToTimestamp(constant(1741380235)).as( + 'unixSecondsToTimestamp' + ), + unixMillisToTimestamp(constant(1741380235123)).as( + 'unixMillisToTimestamp' + ), + unixMicrosToTimestamp(constant(1741380235123456)).as( + 'unixMicrosToTimestamp' + ), + timestampToUnixSeconds( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixSeconds'), + timestampToUnixMicros( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixMicros'), + timestampToUnixMillis( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixMillis') + ) + ); + expectResults(snapshot, { + unixMicrosToTimestamp: new Timestamp(1741380235, 123456000), + unixMillisToTimestamp: new Timestamp(1741380235, 123000000), + unixSecondsToTimestamp: new Timestamp(1741380235, 0), + timestampToUnixSeconds: 1741380235, + timestampToUnixMicros: 1741380235123456, + timestampToUnixMillis: 1741380235123 + }); + }); + + it('supports timestamp math', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(new Timestamp(1741380235, 0)).as('timestamp')) + .select( + timestampAdd('timestamp', 'day', 10).as('plus10days'), + timestampAdd('timestamp', 'hour', 10).as('plus10hours'), + timestampAdd('timestamp', 'minute', 10).as('plus10minutes'), + timestampAdd('timestamp', 'second', 10).as('plus10seconds'), + timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), + timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), + timestampSub('timestamp', 'day', 10).as('minus10days'), + timestampSub('timestamp', 'hour', 10).as('minus10hours'), + timestampSub('timestamp', 'minute', 10).as('minus10minutes'), + timestampSub('timestamp', 'second', 10).as('minus10seconds'), + timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), + timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + ) + ); + expectResults(snapshot, { + plus10days: new Timestamp(1742244235, 0), + plus10hours: new Timestamp(1741416235, 0), + plus10minutes: new Timestamp(1741380835, 0), + plus10seconds: new Timestamp(1741380245, 0), + plus10micros: new Timestamp(1741380235, 10000), + plus10millis: new Timestamp(1741380235, 10000000), + minus10days: new Timestamp(1740516235, 0), + minus10hours: new Timestamp(1741344235, 0), + minus10minutes: new Timestamp(1741379635, 0), + minus10seconds: new Timestamp(1741380225, 0), + minus10micros: new Timestamp(1741380234, 999990000), + minus10millis: new Timestamp(1741380234, 990000000) + }); + }); + + it('supports byteLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .select( + constant( + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) + ).as('bytes') + ) + .select(byteLength('bytes').as('byteLength')) + ); + + expectResults(snapshot, { + byteLength: 8 + }); + }); + + it('supports not', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .select(constant(true).as('trueField')) + .select('trueField', not(eq('trueField', true)).as('falseField')) + ); + + expectResults(snapshot, { + trueField: true, + falseField: false + }); + }); + }); + + describe('not yet implemented in backend', () => { + itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitAnd(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 4 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitAnd(12).as('result')) + ); + expectResults(snapshot, { + result: 4 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_or', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitOr(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 13 + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitOr(12).as('result')) + ); + expectResults(snapshot, { + result: 13 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitXor(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 9 + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitXor(12).as('result')) + ); + expectResults(snapshot, { + result: 9 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_not', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( + 'result' + ) + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) + .bitNot() + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitLeftShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitLeftShift(2) + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitRightShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitRightShift(2) + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(documentId(field('__path__')).as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('__path__').documentId().as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + }); + + itIf(testUnsupportedFeatures)('supports Substr', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); + + itIf(testUnsupportedFeatures)( + 'supports Substr without length', + async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + } + ); + + itIf(testUnsupportedFeatures)('arrayConcat works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + arrayConcat('tags', ['newTag1', 'newTag2'], field('tags'), [ + null + ]).as('modifiedTags') + ) + .limit(1) + ); + expectResults(snapshot, { + modifiedTags: [ + 'comedy', + 'space', + 'adventure', + 'newTag1', + 'newTag2', + 'comedy', + 'space', + 'adventure', + null + ] + }); + }); + + itIf(testUnsupportedFeatures)('testToLowercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(toLower('title').as('lowercaseTitle')) + .limit(1) + ); + expectResults(snapshot, { + lowercaseTitle: "the hitchhiker's guide to the galaxy" + }); + }); + + itIf(testUnsupportedFeatures)('testToUppercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(toUpper('author').as('uppercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + }); + + itIf(testUnsupportedFeatures)('testTrim', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + constant(" The Hitchhiker's Guide to the Galaxy ").as('spacedTitle') + ) + .select(trim('spacedTitle').as('trimmedTitle'), field('spacedTitle')) + .limit(1) + ); + expectResults(snapshot, { + spacedTitle: " The Hitchhiker's Guide to the Galaxy ", + trimmedTitle: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + itIf(testUnsupportedFeatures)('test reverse', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', '1984')) + .limit(1) + .select(reverse('title').as('reverseTitle')) + ); + expectResults(snapshot, { title: '4891' }); + }); + }); + + describe('pagination', () => { + /** + * Adds several books to the test collection. These + * additional books support pagination test scenarios + * that would otherwise not be possible with the original + * set of books. + * @param collectionReference + */ + async function addBooks( + collectionReference: CollectionReference + ): Promise { + await setDoc(doc(collectionReference, 'book11'), { + title: 'Jonathan Strange & Mr Norrell', + author: 'Susanna Clarke', + genre: 'Fantasy', + published: 2004, + rating: 4.6, + tags: ['historical fantasy', 'magic', 'alternate history', 'england'], + awards: { hugo: false, nebula: false } + }); + await setDoc(doc(collectionReference, 'book12'), { + title: 'The Master and Margarita', + author: 'Mikhail Bulgakov', + genre: 'Satire', + published: 1967, // Though written much earlier + rating: 4.6, + tags: [ + 'russian literature', + 'supernatural', + 'philosophy', + 'dark comedy' + ], + awards: {} + }); + await setDoc(doc(collectionReference, 'book13'), { + title: 'A Long Way to a Small, Angry Planet', + author: 'Becky Chambers', + genre: 'Science Fiction', + published: 2014, + rating: 4.6, + tags: ['space opera', 'found family', 'character-driven', 'optimistic'], + awards: { hugo: false, nebula: false, kitschies: true } + }); + } + + // sort on __name__ is not working, see b/409358591 + itIf(testUnsupportedFeatures)( + 'supports pagination with filters', + async () => { + await addBooks(randomCol); + const pageSize = 2; + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', '__name__') + .sort(field('rating').descending(), field('__name__').ascending()); + + let snapshot = await execute(pipeline.limit(pageSize)); + expectResults( + snapshot, + { title: 'The Lord of the Rings', rating: 4.7 }, + { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 } + ); + + const lastDoc = snapshot.results[snapshot.results.length - 1]; + + snapshot = await execute( + pipeline + .where( + or( + and( + field('rating').eq(lastDoc.get('rating')), + field('__path__').gt(lastDoc.ref?.id) + ), + field('rating').lt(lastDoc.get('rating')) + ) + ) + .limit(pageSize) + ); + expectResults( + snapshot, + { title: 'Pride and Prejudice', rating: 4.5 }, + { title: 'Crime and Punishment', rating: 4.3 } + ); + } + ); + + // sort on __name__ is not working, see b/409358591 + itIf(testUnsupportedFeatures)( + 'supports pagination with offsets', + async () => { + await addBooks(randomCol); + + const secondFilterField = '__path__'; + + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', secondFilterField) + .sort( + field('rating').descending(), + field(secondFilterField).ascending() + ); + + const pageSize = 2; + let currPage = 0; + + let snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + + expectResults( + snapshot, + { + title: 'The Lord of the Rings', + rating: 4.7 + }, + { title: 'Dune', rating: 4.6 } + ); + + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'Jonathan Strange & Mr Norrell', + rating: 4.6 + }, + { title: 'The Master and Margarita', rating: 4.6 } + ); + + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 + }, + { + title: 'Pride and Prejudice', + rating: 4.5 + } + ); + } + ); + }); + + describe('console support', () => { + it('supports internal serialization to proto', async () => { + const pipeline = firestore + .pipeline() + .collection('books') + .where(eq('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + ); + + const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); + expect(proto).not.to.be.null; + }); + }); +}); diff --git a/packages/firestore/test/integration/api/query_to_pipeline.test.ts b/packages/firestore/test/integration/api/query_to_pipeline.test.ts new file mode 100644 index 00000000000..ac1d2e79007 --- /dev/null +++ b/packages/firestore/test/integration/api/query_to_pipeline.test.ts @@ -0,0 +1,820 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; +import { addEqualityMatcher } from '../../util/equality_matcher'; +import { + doc, + DocumentData, + setDoc, + setLogLevel, + query, + where, + FieldPath, + orderBy, + limit, + limitToLast, + startAt, + startAfter, + endAt, + endBefore, + collectionGroup, + collection, + and, + documentId, + addDoc, + getDoc, + or +} from '../util/firebase_export'; +import { + apiDescribe, + PERSISTENCE_MODE_UNSPECIFIED, + withTestCollection, + itIf +} from '../util/helpers'; +import { execute } from '../util/pipeline_export'; + +use(chaiAsPromised); + +setLogLevel('debug'); + +const testUnsupportedFeatures: boolean | 'only' = false; + +// This is the Query integration tests from the lite API (no cache support) +// with some additional test cases added for more complete coverage. +apiDescribe('Query to Pipeline', persistence => { + addEqualityMatcher(); + + function verifyResults( + actual: PipelineSnapshot, + ...expected: DocumentData[] + ): void { + const results = actual.results; + expect(results.length).to.equal(expected.length); + + for (let i = 0; i < expected.length; ++i) { + expect(results[i].data()).to.deep.equal(expected[i]); + } + } + + it('supports default query', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { 1: { foo: 1 } }, + async (collRef, db) => { + const snapshot = await execute(db.pipeline().createFrom(collRef)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports filtered query', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('foo', '==', 1)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports filtered query (with FieldPath)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, where(new FieldPath('foo'), '==', 1)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports ordered query (with default order)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo')); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }, { foo: 2 }); + } + ); + }); + + it('supports ordered query (with asc)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo', 'asc')); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }, { foo: 2 }); + } + ); + }); + + it('supports ordered query (with desc)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo', 'desc')); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }, { foo: 1 }); + } + ); + }); + + it('supports limit query', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), limit(1)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports limitToLast query', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 }, + 3: { foo: 3 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), limitToLast(2)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }, { foo: 3 }); + } + ); + }); + + it('supports startAt', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), startAt(2)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }); + } + ); + }); + + it('supports startAt with limitToLast', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 }, + 3: { foo: 3 }, + 4: { foo: 4 }, + 5: { foo: 5 } + }, + async (collRef, db) => { + const query1 = query( + collRef, + orderBy('foo'), + startAt(3), + limitToLast(4) + ); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 3 }, { foo: 4 }, { foo: 5 }); + } + ); + }); + + it('supports endAt with limitToLast', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 }, + 3: { foo: 3 }, + 4: { foo: 4 }, + 5: { foo: 5 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), endAt(3), limitToLast(2)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }, { foo: 3 }); + } + ); + }); + + it('supports startAfter (with DocumentSnapshot)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { id: 1, foo: 1, bar: 1, baz: 1 }, + 2: { id: 2, foo: 1, bar: 1, baz: 2 }, + 3: { id: 3, foo: 1, bar: 1, baz: 2 }, + 4: { id: 4, foo: 1, bar: 2, baz: 1 }, + 5: { id: 5, foo: 1, bar: 2, baz: 2 }, + 6: { id: 6, foo: 1, bar: 2, baz: 2 }, + 7: { id: 7, foo: 2, bar: 1, baz: 1 }, + 8: { id: 8, foo: 2, bar: 1, baz: 2 }, + 9: { id: 9, foo: 2, bar: 1, baz: 2 }, + 10: { id: 10, foo: 2, bar: 2, baz: 1 }, + 11: { id: 11, foo: 2, bar: 2, baz: 2 }, + 12: { id: 12, foo: 2, bar: 2, baz: 2 } + }, + async (collRef, db) => { + let docRef = await getDoc(doc(collRef, '2')); + let query1 = query( + collRef, + orderBy('foo'), + orderBy('bar'), + orderBy('baz'), + startAfter(docRef) + ); + let snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 3, foo: 1, bar: 1, baz: 2 }, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 5, foo: 1, bar: 2, baz: 2 }, + { id: 6, foo: 1, bar: 2, baz: 2 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 8, foo: 2, bar: 1, baz: 2 }, + { id: 9, foo: 2, bar: 1, baz: 2 }, + { id: 10, foo: 2, bar: 2, baz: 1 }, + { id: 11, foo: 2, bar: 2, baz: 2 }, + { id: 12, foo: 2, bar: 2, baz: 2 } + ); + + docRef = await getDoc(doc(collRef, '3')); + query1 = query( + collRef, + orderBy('foo'), + orderBy('bar'), + orderBy('baz'), + startAfter(docRef) + ); + snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 5, foo: 1, bar: 2, baz: 2 }, + { id: 6, foo: 1, bar: 2, baz: 2 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 8, foo: 2, bar: 1, baz: 2 }, + { id: 9, foo: 2, bar: 1, baz: 2 }, + { id: 10, foo: 2, bar: 2, baz: 1 }, + { id: 11, foo: 2, bar: 2, baz: 2 }, + { id: 12, foo: 2, bar: 2, baz: 2 } + ); + } + ); + }); + + it('supports startAt (with DocumentSnapshot)', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { id: 1, foo: 1, bar: 1, baz: 1 }, + 2: { id: 2, foo: 1, bar: 1, baz: 2 }, + 3: { id: 3, foo: 1, bar: 1, baz: 2 }, + 4: { id: 4, foo: 1, bar: 2, baz: 1 }, + 5: { id: 5, foo: 1, bar: 2, baz: 2 }, + 6: { id: 6, foo: 1, bar: 2, baz: 2 }, + 7: { id: 7, foo: 2, bar: 1, baz: 1 }, + 8: { id: 8, foo: 2, bar: 1, baz: 2 }, + 9: { id: 9, foo: 2, bar: 1, baz: 2 }, + 10: { id: 10, foo: 2, bar: 2, baz: 1 }, + 11: { id: 11, foo: 2, bar: 2, baz: 2 }, + 12: { id: 12, foo: 2, bar: 2, baz: 2 } + }, + async (collRef, db) => { + let docRef = await getDoc(doc(collRef, '2')); + let query1 = query( + collRef, + orderBy('foo'), + orderBy('bar'), + orderBy('baz'), + startAt(docRef) + ); + let snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 2, foo: 1, bar: 1, baz: 2 }, + { id: 3, foo: 1, bar: 1, baz: 2 }, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 5, foo: 1, bar: 2, baz: 2 }, + { id: 6, foo: 1, bar: 2, baz: 2 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 8, foo: 2, bar: 1, baz: 2 }, + { id: 9, foo: 2, bar: 1, baz: 2 }, + { id: 10, foo: 2, bar: 2, baz: 1 }, + { id: 11, foo: 2, bar: 2, baz: 2 }, + { id: 12, foo: 2, bar: 2, baz: 2 } + ); + + docRef = await getDoc(doc(collRef, '3')); + query1 = query( + collRef, + orderBy('foo'), + orderBy('bar'), + orderBy('baz'), + startAt(docRef) + ); + snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 3, foo: 1, bar: 1, baz: 2 }, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 5, foo: 1, bar: 2, baz: 2 }, + { id: 6, foo: 1, bar: 2, baz: 2 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 8, foo: 2, bar: 1, baz: 2 }, + { id: 9, foo: 2, bar: 1, baz: 2 }, + { id: 10, foo: 2, bar: 2, baz: 1 }, + { id: 11, foo: 2, bar: 2, baz: 2 }, + { id: 12, foo: 2, bar: 2, baz: 2 } + ); + } + ); + }); + + it('supports startAfter', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), startAfter(1)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }); + } + ); + }); + + it('supports endAt', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), endAt(1)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports endBefore', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + const query1 = query(collRef, orderBy('foo'), endBefore(2)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports pagination', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + let query1 = query(collRef, orderBy('foo'), limit(1)); + const pipeline1 = db.pipeline().createFrom(query1); + let snapshot = await execute(pipeline1); + verifyResults(snapshot, { foo: 1 }); + + // Pass the document snapshot from the previous snapshot + query1 = query(query1, startAfter(snapshot.results[0].get('foo'))); + snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }); + } + ); + }); + + it('supports pagination on DocumentIds', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1 }, + 2: { foo: 2 } + }, + async (collRef, db) => { + let query1 = query( + collRef, + orderBy('foo'), + orderBy(documentId(), 'asc'), + limit(1) + ); + const pipeline1 = db.pipeline().createFrom(query1); + let snapshot = await execute(pipeline1); + verifyResults(snapshot, { foo: 1 }); + + // Pass the document snapshot from the previous snapshot + query1 = query( + query1, + startAfter( + snapshot.results[0].get('foo'), + snapshot.results[0].ref?.id + ) + ); + snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2 }); + } + ); + }); + + // needs subcollection support + itIf(testUnsupportedFeatures)('supports collection groups', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + {}, + async (collRef, db) => { + const collectionGroupId = `${collRef.id}group`; + + const fooDoc = doc( + collRef.firestore, + `${collRef.id}/foo/${collectionGroupId}/doc1` + ); + const barDoc = doc( + collRef.firestore, + `${collRef.id}/bar/baz/boo/${collectionGroupId}/doc2` + ); + await setDoc(fooDoc, { foo: 1 }); + await setDoc(barDoc, { bar: 1 }); + + const query1 = collectionGroup(collRef.firestore, collectionGroupId); + const snapshot = await execute(db.pipeline().createFrom(query1)); + + verifyResults(snapshot, { bar: 1 }, { foo: 1 }); + } + ); + }); + + // needs subcollection support + itIf(testUnsupportedFeatures)( + 'supports query over collection path with special characters', + () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + {}, + async (collRef, db) => { + const docWithSpecials = doc(collRef, 'so!@#$%^&*()_+special'); + + const collectionWithSpecials = collection( + docWithSpecials, + 'so!@#$%^&*()_+special' + ); + await addDoc(collectionWithSpecials, { foo: 1 }); + await addDoc(collectionWithSpecials, { foo: 2 }); + + const snapshot = await execute( + db + .pipeline() + .createFrom(query(collectionWithSpecials, orderBy('foo', 'asc'))) + ); + + verifyResults(snapshot, { foo: 1 }, { foo: 2 }); + } + ); + } + ); + + it('supports multiple inequality on same field', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + '01': { id: 1, foo: 1, bar: 1, baz: 1 }, + '02': { id: 2, foo: 1, bar: 1, baz: 2 }, + '03': { id: 3, foo: 1, bar: 1, baz: 2 }, + '04': { id: 4, foo: 1, bar: 2, baz: 1 }, + '05': { id: 5, foo: 1, bar: 2, baz: 2 }, + '06': { id: 6, foo: 1, bar: 2, baz: 2 }, + '07': { id: 7, foo: 2, bar: 1, baz: 1 }, + '08': { id: 8, foo: 2, bar: 1, baz: 2 }, + '09': { id: 9, foo: 2, bar: 1, baz: 2 }, + '10': { id: 10, foo: 2, bar: 2, baz: 1 }, + '11': { id: 11, foo: 2, bar: 2, baz: 2 }, + '12': { id: 12, foo: 2, bar: 2, baz: 2 } + }, + async (collRef, db) => { + const query1 = query( + collRef, + and(where('id', '>', 2), where('id', '<=', 10)) + ); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 3, foo: 1, bar: 1, baz: 2 }, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 5, foo: 1, bar: 2, baz: 2 }, + { id: 6, foo: 1, bar: 2, baz: 2 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 8, foo: 2, bar: 1, baz: 2 }, + { id: 9, foo: 2, bar: 1, baz: 2 }, + { id: 10, foo: 2, bar: 2, baz: 1 } + ); + } + ); + }); + + it('supports multiple inequality on different fields', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + '01': { id: 1, foo: 1, bar: 1, baz: 1 }, + '02': { id: 2, foo: 1, bar: 1, baz: 2 }, + '03': { id: 3, foo: 1, bar: 1, baz: 2 }, + '04': { id: 4, foo: 1, bar: 2, baz: 1 }, + '05': { id: 5, foo: 1, bar: 2, baz: 2 }, + '06': { id: 6, foo: 1, bar: 2, baz: 2 }, + '07': { id: 7, foo: 2, bar: 1, baz: 1 }, + '08': { id: 8, foo: 2, bar: 1, baz: 2 }, + '09': { id: 9, foo: 2, bar: 1, baz: 2 }, + '10': { id: 10, foo: 2, bar: 2, baz: 1 }, + '11': { id: 11, foo: 2, bar: 2, baz: 2 }, + '12': { id: 12, foo: 2, bar: 2, baz: 2 } + }, + async (collRef, db) => { + const query1 = query( + collRef, + and(where('id', '>=', 2), where('baz', '<', 2)) + ); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { id: 4, foo: 1, bar: 2, baz: 1 }, + { id: 7, foo: 2, bar: 1, baz: 1 }, + { id: 10, foo: 2, bar: 2, baz: 1 } + ); + } + ); + }); + + it('supports collectionGroup query', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { 1: { foo: 1 } }, + async (collRef, db) => { + const snapshot = await execute( + db.pipeline().createFrom(collectionGroup(db, collRef.id)) + ); + verifyResults(snapshot, { foo: 1 }); + } + ); + }); + + it('supports eq nan', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: NaN }, + 2: { foo: 2, bar: 1 }, + 3: { foo: 3, bar: 'bar' } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', '==', NaN)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: NaN }); + } + ); + }); + + it('supports neq nan', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: NaN }, + 2: { foo: 2, bar: 1 }, + 3: { foo: 3, bar: 'bar' } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', '!=', NaN)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2, bar: 1 }); + } + ); + }); + + it('supports eq null', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: null }, + 2: { foo: 2, bar: 1 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', '==', null)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: null }); + } + ); + }); + + it('supports neq null', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: null }, + 2: { foo: 2, bar: 1 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', '!=', null)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2, bar: 1 }); + } + ); + }); + + it('supports neq', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 0 }, + 2: { foo: 2, bar: 1 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', '!=', 0)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 2, bar: 1 }); + } + ); + }); + + it('supports array contains', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: [0, 2, 4, 6] }, + 2: { foo: 2, bar: [1, 3, 5, 7] } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', 'array-contains', 4)); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: [0, 2, 4, 6] }); + } + ); + }); + + it('supports array contains any', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: [0, 2, 4, 6] }, + 2: { foo: 2, bar: [1, 3, 5, 7] }, + 3: { foo: 3, bar: [10, 20, 30, 40] } + }, + async (collRef, db) => { + const query1 = query( + collRef, + where('bar', 'array-contains-any', [4, 5]) + ); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults( + snapshot, + { foo: 1, bar: [0, 2, 4, 6] }, + { foo: 2, bar: [1, 3, 5, 7] } + ); + } + ); + }); + + it('supports in', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 2 }, + 2: { foo: 2 }, + 3: { foo: 3, bar: 10 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', 'in', [0, 10, 20])); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 3, bar: 10 }); + } + ); + }); + + it('supports in with 1', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 2 }, + 2: { foo: 2 }, + 3: { foo: 3, bar: 10 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', 'in', [2])); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: 2 }); + } + ); + }); + + itIf(testUnsupportedFeatures)('supports not in', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 2 }, + 2: { foo: 2, bar: 1 }, + 3: { foo: 3, bar: 10 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', 'not-in', [0, 10, 20])); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: 2 }, { foo: 2, bar: 1 }); + } + ); + }); + + it('supports not in with 1', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 2 }, + 2: { foo: 2 }, + 3: { foo: 3, bar: 10 } + }, + async (collRef, db) => { + const query1 = query(collRef, where('bar', 'not-in', [2])); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 3, bar: 10 }); + } + ); + }); + + it('supports or operator', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + { + 1: { foo: 1, bar: 2 }, + 2: { foo: 2, bar: 0 }, + 3: { foo: 3, bar: 10 } + }, + async (collRef, db) => { + const query1 = query( + collRef, + or(where('bar', '==', 2), where('foo', '==', 3)), + orderBy('foo') + ); + const snapshot = await execute(db.pipeline().createFrom(query1)); + verifyResults(snapshot, { foo: 1, bar: 2 }, { foo: 3, bar: 10 }); + } + ); + }); +}); diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index b36ed980295..1ea11fcc30f 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -580,3 +580,10 @@ export async function checkOnlineAndOfflineResultsMatch( expect(expectedDocs).to.deep.equal(toIds(docsFromServer)); } } + +export function itIf( + condition: boolean | 'only' +): Mocha.TestFunction | Mocha.PendingTestFunction { + // eslint-disable-next-line no-restricted-properties + return condition === 'only' ? it.only : condition ? it : it.skip; +} diff --git a/packages/firestore/test/integration/util/pipeline_export.ts b/packages/firestore/test/integration/util/pipeline_export.ts new file mode 100644 index 00000000000..d2495d772a5 --- /dev/null +++ b/packages/firestore/test/integration/util/pipeline_export.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Imports firebase via the raw sources and re-exports it. The +// "/integration/firestore" test suite replaces this file with a +// reference to the minified sources. If you change any exports in this file, +// you need to also adjust "integration/firestore/pipeline_export.ts". + +export * from '../../../pipelines/pipelines'; diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts new file mode 100644 index 00000000000..f919e1aa9de --- /dev/null +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -0,0 +1,3046 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { Bytes } from '../../src/lite-api/bytes'; +import { + Firestore, + getFirestore, + terminate +} from '../../src/lite-api/database'; +import { + field, + and, + array, + arrayOffset, + constant, + add, + subtract, + multiply, + avg, + bitAnd, + substr, + constantVector, + bitLeftShift, + bitNot, + count, + mapMerge, + mapRemove, + bitOr, + ifError, + isAbsent, + isError, + or, + rand, + bitRightShift, + bitXor, + isNotNan, + map, + isNotNull, + isNull, + mod, + documentId, + eq, + neq, + lt, + countIf, + lte, + gt, + arrayConcat, + arrayContains, + arrayContainsAny, + eqAny, + notEqAny, + xor, + cond, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + like, + regexContains, + regexMatch, + strContains, + startsWith, + endsWith, + mapGet, + countAll, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSub, + ascending, + descending, + FunctionExpr, + BooleanExpr, + AggregateFunction, + sum, + strConcat, + arrayContainsAll, + arrayLength, + charLength, + divide, + replaceFirst, + replaceAll, + byteLength, + not, + toLower, + toUpper, + trim +} from '../../src/lite-api/expressions'; +import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; +import { vector } from '../../src/lite-api/field_value_impl'; +import { GeoPoint } from '../../src/lite-api/geo_point'; +import { + pipelineResultEqual, + PipelineSnapshot +} from '../../src/lite-api/pipeline-result'; +import { execute } from '../../src/lite-api/pipeline_impl'; +import { + DocumentData, + CollectionReference, + collection, + doc +} from '../../src/lite-api/reference'; +import { addDoc, setDoc } from '../../src/lite-api/reference_impl'; +import { FindNearestOptions } from '../../src/lite-api/stage'; +import { Timestamp } from '../../src/lite-api/timestamp'; +import { writeBatch } from '../../src/lite-api/write_batch'; +import { itIf } from '../integration/util/helpers'; +import { addEqualityMatcher } from '../util/equality_matcher'; +import { Deferred } from '../util/promise'; + +import { withTestCollection } from './helpers'; + +use(chaiAsPromised); + +const testUnsupportedFeatures = false; +const timestampDeltaMS = 1000; + +describe('Firestore Pipelines', () => { + addEqualityMatcher(); + + let firestore: Firestore; + let randomCol: CollectionReference; + let beginDocCreation: number = 0; + let endDocCreation: number = 0; + + async function testCollectionWithDocs(docs: { + [id: string]: DocumentData; + }): Promise> { + beginDocCreation = new Date().valueOf(); + for (const id in docs) { + if (docs.hasOwnProperty(id)) { + const ref = doc(randomCol, id); + await setDoc(ref, docs[id]); + } + } + endDocCreation = new Date().valueOf(); + return randomCol; + } + + function expectResults(snapshot: PipelineSnapshot, ...docs: string[]): void; + function expectResults( + snapshot: PipelineSnapshot, + ...data: DocumentData[] + ): void; + + function expectResults( + snapshot: PipelineSnapshot, + ...data: DocumentData[] | string[] + ): void { + const docs = snapshot.results; + + expect(docs.length).to.equal(data.length); + + if (data.length > 0) { + if (typeof data[0] === 'string') { + const actualIds = docs.map(doc => doc.id); + expect(actualIds).to.deep.equal(data); + } else { + docs.forEach(r => { + expect(r.data()).to.deep.equal(data.shift()); + }); + } + } + } + + async function setupBookDocs(): Promise> { + const bookDocs: { [id: string]: DocumentData } = { + book1: { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + embedding: vector([10, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + }, + book2: { + title: 'Pride and Prejudice', + author: 'Jane Austen', + genre: 'Romance', + published: 1813, + rating: 4.5, + tags: ['classic', 'social commentary', 'love'], + awards: { none: true }, + embedding: vector([1, 10, 1, 1, 1, 1, 1, 1, 1, 1]) + }, + book3: { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + genre: 'Magical Realism', + published: 1967, + rating: 4.3, + tags: ['family', 'history', 'fantasy'], + awards: { nobel: true, nebula: false }, + embedding: vector([1, 1, 10, 1, 1, 1, 1, 1, 1, 1]) + }, + book4: { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + genre: 'Fantasy', + published: 1954, + rating: 4.7, + tags: ['adventure', 'magic', 'epic'], + awards: { hugo: false, nebula: false }, + remarks: null, + cost: NaN, + embedding: vector([1, 1, 1, 10, 1, 1, 1, 1, 1, 1]) + }, + book5: { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + genre: 'Dystopian', + published: 1985, + rating: 4.1, + tags: ['feminism', 'totalitarianism', 'resistance'], + awards: { 'arthur c. clarke': true, 'booker prize': false }, + embedding: vector([1, 1, 1, 1, 10, 1, 1, 1, 1, 1]) + }, + book6: { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + genre: 'Psychological Thriller', + published: 1866, + rating: 4.3, + tags: ['philosophy', 'crime', 'redemption'], + awards: { none: true }, + embedding: vector([1, 1, 1, 1, 1, 10, 1, 1, 1, 1]) + }, + book7: { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + genre: 'Southern Gothic', + published: 1960, + rating: 4.2, + tags: ['racism', 'injustice', 'coming-of-age'], + awards: { pulitzer: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 10, 1, 1, 1]) + }, + book8: { + title: '1984', + author: 'George Orwell', + genre: 'Dystopian', + published: 1949, + rating: 4.2, + tags: ['surveillance', 'totalitarianism', 'propaganda'], + awards: { prometheus: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 10, 1, 1]) + }, + book9: { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + genre: 'Modernist', + published: 1925, + rating: 4.0, + tags: ['wealth', 'american dream', 'love'], + awards: { none: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 1, 10, 1]) + }, + book10: { + title: 'Dune', + author: 'Frank Herbert', + genre: 'Science Fiction', + published: 1965, + rating: 4.6, + tags: ['politics', 'desert', 'ecology'], + awards: { hugo: true, nebula: true }, + embedding: vector([1, 1, 1, 1, 1, 1, 1, 1, 1, 10]) + } + }; + return testCollectionWithDocs(bookDocs); + } + + let testDeferred: Deferred | undefined; + let withTestCollectionPromise: Promise | undefined; + + beforeEach(async () => { + const setupDeferred = new Deferred(); + testDeferred = new Deferred(); + withTestCollectionPromise = withTestCollection(async collectionRef => { + randomCol = collectionRef; + firestore = collectionRef.firestore; + await setupBookDocs(); + setupDeferred.resolve(); + + return testDeferred?.promise; + }); + + await setupDeferred.promise; + }); + + afterEach(async () => { + testDeferred?.resolve(); + await withTestCollectionPromise; + }); + + describe('pipeline results', () => { + it('empty snapshot as expected', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol.path).limit(0) + ); + expect(snapshot.results.length).to.equal(0); + }); + + // Skipping because __name__ is not currently working in DBE + itIf(testUnsupportedFeatures)('full snapshot as expected', async () => { + const ppl = firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('__name__')); + const snapshot = await execute(ppl); + expect(snapshot.results.length).to.equal(10); + expect(snapshot.pipeline).to.equal(ppl); + expectResults( + snapshot, + 'book1', + 'book10', + 'book2', + 'book3', + 'book4', + 'book5', + 'book6', + 'book7', + 'book8', + 'book9' + ); + }); + + it('result equals works', async () => { + const ppl = firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .limit(1); + const snapshot1 = await execute(ppl); + const snapshot2 = await execute(ppl); + expect(snapshot1.results.length).to.equal(1); + expect(snapshot2.results.length).to.equal(1); + expect(pipelineResultEqual(snapshot1.results[0], snapshot2.results[0])).to + .be.true; + }); + + it('returns execution time', async () => { + const start = new Date().valueOf(); + const pipeline = firestore.pipeline().collection(randomCol.path); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it.only('returns execution time for an empty query', async () => { + const start = new Date().valueOf(); + const pipeline = firestore.pipeline().collection(randomCol.path).limit(0); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.results.length).to.equal(0); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it('returns create and update time for each document', async () => { + const pipeline = firestore.pipeline().collection(randomCol.path); + + let snapshot = await execute(pipeline); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(doc => { + expect(doc.createTime).to.not.be.null; + expect(doc.updateTime).to.not.be.null; + + expect(doc.createTime!.toDate().valueOf()).to.approximately( + (beginDocCreation + endDocCreation) / 2, + timestampDeltaMS + ); + expect(doc.updateTime!.toDate().valueOf()).to.approximately( + (beginDocCreation + endDocCreation) / 2, + timestampDeltaMS + ); + expect(doc.createTime?.valueOf()).to.equal(doc.updateTime?.valueOf()); + }); + + const wb = writeBatch(firestore); + snapshot.results.forEach(doc => { + wb.update(doc.ref!, { newField: 'value' }); + }); + await wb.commit(); + + snapshot = await execute(pipeline); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(doc => { + expect(doc.createTime).to.not.be.null; + expect(doc.updateTime).to.not.be.null; + expect(doc.createTime!.toDate().valueOf()).to.be.lessThan( + doc.updateTime!.toDate().valueOf() + ); + }); + }); + + it('returns execution time for an aggregate query', async () => { + const start = new Date().valueOf(); + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate(avg('rating').as('avgRating')); + + const snapshot = await execute(pipeline); + const end = new Date().valueOf(); + + expect(snapshot.results.length).to.equal(1); + + expect(snapshot.executionTime.toDate().valueOf()).to.approximately( + (start + end) / 2, + timestampDeltaMS + ); + }); + + it('returns undefined create and update time for each result in an aggregate query', async () => { + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [avg('rating').as('avgRating')], + groups: ['genre'] + }); + + const snapshot = await execute(pipeline); + + expect(snapshot.results.length).to.equal(8); + + snapshot.results.forEach(doc => { + expect(doc.updateTime).to.be.undefined; + expect(doc.createTime).to.be.undefined; + }); + }); + }); + + describe('pipeline sources', () => { + it('supports CollectionReference as source', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol) + ); + expect(snapshot.results.length).to.equal(10); + }); + + it('supports list of documents as source', async () => { + const collName = randomCol.id; + + const snapshot = await execute( + firestore + .pipeline() + .documents([ + `${collName}/book1`, + doc(randomCol, 'book2'), + doc(randomCol, 'book3').path + ]) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('reject CollectionReference for another DB', async () => { + const db2 = getFirestore(firestore.app, 'notDefault'); + + expect(() => { + firestore.pipeline().collection(collection(db2, 'foo')); + }).to.throw(/Invalid CollectionReference/); + + await terminate(db2); + }); + + it('reject DocumentReference for another DB', async () => { + const db2 = getFirestore(firestore.app, 'notDefault'); + + expect(() => { + firestore.pipeline().documents([doc(db2, 'foo/bar')]); + }).to.throw(/Invalid DocumentReference/); + + await terminate(db2); + }); + + // Subcollections not currently supported in DBE + itIf(testUnsupportedFeatures)( + 'supports collection group as source', + async () => { + const randomSubCollectionId = Math.random().toString(16).slice(2); + const doc1 = await addDoc( + collection(randomCol, 'book1', randomSubCollectionId), + { order: 1 } + ); + const doc2 = await addDoc( + collection(randomCol, 'book2', randomSubCollectionId), + { order: 2 } + ); + const snapshot = await execute( + firestore + .pipeline() + .collectionGroup(randomSubCollectionId) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + } + ); + + // subcollections not currently supported in dbe + itIf(testUnsupportedFeatures)('supports database as source', async () => { + const randomId = Math.random().toString(16).slice(2); + const doc1 = await addDoc(collection(randomCol, 'book1', 'sub'), { + order: 1, + randomId + }); + const doc2 = await addDoc(collection(randomCol, 'book2', 'sub'), { + order: 2, + randomId + }); + const snapshot = await execute( + firestore + .pipeline() + .database() + .where(eq('randomId', randomId)) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + }); + }); + + describe('supported data types', () => { + it('accepts and returns all data types', async () => { + const refDate = new Date(); + const refTimestamp = Timestamp.now(); + const constants = [ + constant(1).as('number'), + constant('a string').as('string'), + constant(true).as('boolean'), + constant(null).as('null'), + constant(new GeoPoint(0.1, 0.2)).as('geoPoint'), + constant(refTimestamp).as('timestamp'), + constant(refDate).as('date'), + constant( + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) + ).as('bytes'), + constant(doc(firestore, 'foo', 'bar')).as('documentReference'), + constantVector(vector([1, 2, 3])).as('vectorValue'), + constantVector([1, 2, 3]).as('vectorValue2'), + map({ + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': refDate, + 'uint8Array': Bytes.fromUint8Array( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0]) + ), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'map': { + 'number': 2, + 'string': 'b string' + }, + 'array': [1, 'c string'] + }).as('map'), + array([ + 1, + 'a string', + true, + null, + new GeoPoint(0.1, 0.2), + refTimestamp, + refDate, + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + doc(firestore, 'foo', 'bar'), + vector([1, 2, 3]), + { + 'number': 2, + 'string': 'b string' + } + ]).as('array') + ]; + + const snapshots = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constants[0], ...constants.slice(1)) + ); + + expectResults(snapshots, { + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': Timestamp.fromDate(refDate), + 'bytes': Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'vectorValue2': vector([1, 2, 3]), + 'map': { + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': new GeoPoint(0.1, 0.2), + 'timestamp': refTimestamp, + 'date': Timestamp.fromDate(refDate), + 'uint8Array': Bytes.fromUint8Array( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0]) + ), + 'documentReference': doc(firestore, 'foo', 'bar'), + 'vectorValue': vector([1, 2, 3]), + 'map': { + 'number': 2, + 'string': 'b string' + }, + 'array': [1, 'c string'] + }, + 'array': [ + 1, + 'a string', + true, + null, + new GeoPoint(0.1, 0.2), + refTimestamp, + Timestamp.fromDate(refDate), + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), + doc(firestore, 'foo', 'bar'), + vector([1, 2, 3]), + { + 'number': 2, + 'string': 'b string' + } + ] + }); + }); + + it('throws on undefined in a map', async () => { + expect(() => { + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + map({ + 'number': 1, + undefined + }).as('foo') + ); + }).to.throw( + 'Function map() called with invalid data. Unsupported field value: undefined' + ); + }); + + it('throws on undefined in an array', async () => { + expect(() => { + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(array([1, undefined]).as('foo')); + }).to.throw( + 'Function array() called with invalid data. Unsupported field value: undefined' + ); + }); + + it('converts arrays and plain objects to functionValues if the customer intent is unspecified', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + 'title', + 'author', + 'genre', + 'rating', + 'published', + 'tags', + 'awards' + ) + .addFields( + array([ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]).as('metadataArray'), + map({ + genre: field('genre'), + rating: multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: { + published: field('published') + } + }).as('metadata') + ) + .where( + and( + eq('metadataArray', [ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]), + eq('metadata', { + genre: field('genre'), + rating: multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: { + published: field('published') + } + }) + ) + ) + ); + + expect(snapshot.results.length).to.equal(1); + + expectResults(snapshot, { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + genre: 'Fantasy', + published: 1954, + rating: 4.7, + tags: ['adventure', 'magic', 'epic'], + awards: { hugo: false, nebula: false }, + metadataArray: [ + 1, + 2, + 'Fantasy', + 47, + ['The Lord of the Rings'], + { + published: 1954 + } + ], + metadata: { + genre: 'Fantasy', + rating: 47, + nestedArray: ['The Lord of the Rings'], + nestedMap: { + published: 1954 + } + } + }); + }); + }); + + describe('stages', () => { + describe('aggregate stage', () => { + it('supports aggregate', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countAll().as('count')) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + avg('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + ) + ); + expectResults(snapshot, { + count: 2, + avgRating: 4.4, + maxRating: 4.6, + sumRating: 8.8 + }); + }); + + it('rejects groups without accumulators', async () => { + await expect( + execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(lt('published', 1900)) + .aggregate({ + accumulators: [], + groups: ['genre'] + }) + ) + ).to.be.rejected; + }); + + it('returns group and accumulate results', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(lt(field('published'), 1984)) + .aggregate({ + accumulators: [avg('rating').as('avgRating')], + groups: ['genre'] + }) + .where(gt('avgRating', 4.3)) + .sort(field('avgRating').descending()) + ); + expectResults( + snapshot, + { avgRating: 4.7, genre: 'Fantasy' }, + { avgRating: 4.5, genre: 'Romance' }, + { avgRating: 4.4, genre: 'Science Fiction' } + ); + }); + + it('returns min, max, count, and countAll accumulations', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + count('cost').as('booksWithCost'), + countAll().as('count'), + maximum('rating').as('maxRating'), + minimum('published').as('minPublished') + ) + ); + expectResults(snapshot, { + booksWithCost: 1, + count: 10, + maxRating: 4.7, + minPublished: 1813 + }); + }); + + it('returns countif accumulation', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countIf(field('rating').gt(4.3)).as('count')) + ); + const expectedResults = { + count: 3 + }; + expectResults(snapshot, expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(field('rating').gt(4.3).countIf().as('count')) + ); + expectResults(snapshot, expectedResults); + }); + }); + + describe('distinct stage', () => { + it('returns distinct values as expected', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort(field('genre').ascending(), field('author').ascending()) + ); + expectResults( + snapshot, + { genre: 'Dystopian', author: 'George Orwell' }, + { genre: 'Dystopian', author: 'Margaret Atwood' }, + { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, + { genre: 'Magical Realism', author: 'Gabriel García Márquez' }, + { genre: 'Modernist', author: 'F. Scott Fitzgerald' }, + { genre: 'Psychological Thriller', author: 'Fyodor Dostoevsky' }, + { genre: 'Romance', author: 'Jane Austen' }, + { genre: 'Science Fiction', author: 'Douglas Adams' }, + { genre: 'Science Fiction', author: 'Frank Herbert' }, + { genre: 'Southern Gothic', author: 'Harper Lee' } + ); + }); + }); + + describe('select stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }, + { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, + { title: 'Dune', author: 'Frank Herbert' }, + { title: 'Crime and Punishment', author: 'Fyodor Dostoevsky' }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez' + }, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' }, + { title: 'Pride and Prejudice', author: 'Jane Austen' }, + { title: "The Handmaid's Tale", author: 'Margaret Atwood' } + ); + }); + }); + + describe('addField stage', () => { + it('can add fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields(constant('bar').as('foo')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + foo: 'bar' + }, + { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + foo: 'bar' + }, + { title: 'Dune', author: 'Frank Herbert', foo: 'bar' }, + { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + foo: 'bar' + }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + foo: 'bar' + }, + { title: '1984', author: 'George Orwell', foo: 'bar' }, + { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + foo: 'bar' + }, + { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + foo: 'bar' + }, + { title: 'Pride and Prejudice', author: 'Jane Austen', foo: 'bar' }, + { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + foo: 'bar' + } + ); + }); + }); + + describe('removeFields stage', () => { + it('can remove fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + .removeFields(field('author')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + }); + + describe('where stage', () => { + it('where with and (2 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.5), + eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + ) + ); + expectResults(snapshot, 'book10', 'book4'); + }); + it('where with and (3 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.5), + eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lt('published', 1965) + ) + ) + ); + expectResults(snapshot, 'book4'); + }); + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy') + ) + ) + .sort(ascending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: '1984' }, + { title: 'Pride and Prejudice' }, + { title: "The Handmaid's Tale" }, + { title: 'The Lord of the Rings' } + ); + }); + + it('where with xor', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + xor( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy'), + eq('published', 1949) + ) + ) + .select('title') + ); + expectResults( + snapshot, + { title: 'Pride and Prejudice' }, + { title: 'The Lord of the Rings' }, + { title: "The Handmaid's Tale" } + ); + }); + }); + + describe('sort, offset, and limit stages', () => { + it('supports sort, offset, and limits', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('author').ascending()) + .offset(5) + .limit(3) + .select('title', 'author') + ); + expectResults( + snapshot, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } + ); + }); + }); + + describe('generic stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .genericStage('select', [ + { + title: field('title'), + metadata: { + 'author': field('author') + } + } + ]) + .sort(field('author').ascending()) + .limit(1) + ); + expectResults(snapshot, { + metadata: { + author: 'Frank Herbert' + }, + title: 'Dune' + }); + }); + + it('can add fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('author').ascending()) + .limit(1) + .select('title', 'author') + .genericStage('add_fields', [ + { + display: strConcat('title', ' - ', field('author')) + } + ]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + display: "The Hitchhiker's Guide to the Galaxy - Douglas Adams" + }); + }); + + it('can filter with where', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('where', [field('author').eq('Douglas Adams')]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }); + }); + + it('can limit, offset, and sort', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('sort', [ + { + direction: 'ascending', + expression: field('author') + } + ]) + .genericStage('offset', [3]) + .genericStage('limit', [1]) + ); + expectResults(snapshot, { + author: 'Fyodor Dostoevsky', + title: 'Crime and Punishment' + }); + }); + + it('can perform aggregate query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('aggregate', [ + { averageRating: field('rating').avg() }, + {} + ]) + ); + expectResults(snapshot, { + averageRating: 4.3100000000000005 + }); + }); + + it('can perform distinct query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('distinct', [{ rating: field('rating') }]) + .sort(field('rating').descending()) + ); + expectResults( + snapshot, + { + rating: 4.7 + }, + { + rating: 4.6 + }, + { + rating: 4.5 + }, + { + rating: 4.3 + }, + { + rating: 4.2 + }, + { + rating: 4.1 + }, + { + rating: 4.0 + } + ); + }); + }); + + describe('replaceWith stage', () => { + it('run pipeline with replaceWith field name', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith('awards') + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }); + }); + + it('run pipeline with replaceWith Expr result', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith( + map({ + foo: 'bar', + baz: { + title: field('title') + } + }) + ) + ); + expectResults(snapshot, { + foo: 'bar', + baz: { title: "The Hitchhiker's Guide to the Galaxy" } + }); + }); + }); + + describe('sample stage', () => { + it('run pipeline with sample limit of 3', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol.path).sample(3) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {documents: 3}', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ documents: 3 }) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {percentage: 0.6}', async () => { + let avgSize = 0; + const numIterations = 20; + for (let i = 0; i < numIterations; i++) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ percentage: 0.6 }) + ); + + avgSize += snapshot.results.length; + } + avgSize /= numIterations; + expect(avgSize).to.be.closeTo(6, 1); + }); + }); + + describe('union stage', () => { + // __name__ not currently supported by dbe + itIf(testUnsupportedFeatures)('run pipeline with union', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union(firestore.pipeline().collection(randomCol.path)) + .sort(field(documentIdFieldPath()).ascending()) + ); + expectResults( + snapshot, + 'book1', + 'book1', + 'book10', + 'book10', + 'book2', + 'book2', + 'book3', + 'book3', + 'book4', + 'book4', + 'book5', + 'book5', + 'book6', + 'book6', + 'book7', + 'book7', + 'book8', + 'book8', + 'book9', + 'book9' + ); + }); + }); + + describe('unnest stage', () => { + it('run pipeline with unnest', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag')) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + it('unnest an expr', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(array([1, 2, 3]).as('copy')) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'copy', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 1, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 2, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 3, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + }); + + describe('findNearest stage', () => { + it('run pipeline with findNearest', async () => { + const measures: Array = [ + 'euclidean', + 'dot_product', + 'cosine' + ]; + for (const measure of measures) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), + limit: 3, + distanceMeasure: measure + }) + .select('title') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'One Hundred Years of Solitude' + }, + { + title: "The Handmaid's Tale" + } + ); + } + }); + + it('optionally returns the computed distance', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + limit: 2, + distanceMeasure: 'euclidean', + distanceField: 'computedDistance' + }) + .select('title', 'computedDistance') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + computedDistance: 1 + }, + { + title: 'One Hundred Years of Solitude', + computedDistance: 12.041594578792296 + } + ); + }); + }); + }); + + describe('function expressions', () => { + it('logical max works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMaximum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1961 }, + { title: 'Crime and Punishment', 'published-safe': 1961 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); + + it('logical min works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMinimum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1949 }, + { title: 'Crime and Punishment', 'published-safe': 1866 }, + { title: 'Dune', 'published-safe': 1960 } + ); + }); + + it('cond works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + cond( + lt(field('published'), 1960), + constant(1960), + field('published') + ).as('published-safe') + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1960 }, + { title: 'Crime and Punishment', 'published-safe': 1960 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); + + it('eqAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eqAny('published', [1979, 1999, 1967])) + .sort(descending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'One Hundred Years of Solitude' } + ); + }); + + it('notEqAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + notEqAny( + 'published', + [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] + ) + ) + .select('title') + ); + expectResults(snapshot, { title: 'Pride and Prejudice' }); + }); + + it('arrayContains works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContains('tags', 'comedy')) + .select('title') + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('arrayContainsAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContainsAny('tags', ['comedy', 'classic'])) + .sort(descending('title')) + .select('title') + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'Pride and Prejudice' } + ); + }); + + it('arrayContainsAll works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(arrayContainsAll('tags', ['adventure', 'magic'])) + .select('title') + ); + expectResults(snapshot, { title: 'The Lord of the Rings' }); + }); + + it('arrayLength works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(arrayLength('tags').as('tagsCount')) + .where(eq('tagsCount', 3)) + ); + expect(snapshot.results.length).to.equal(10); + }); + + it('testStrConcat', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('author')) + .select( + field('author').strConcat(' - ', field('title')).as('bookInfo') + ) + .limit(1) + ); + expectResults(snapshot, { + bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('testStartsWith', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(startsWith('title', 'The')) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: 'The Great Gatsby' }, + { title: "The Handmaid's Tale" }, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'The Lord of the Rings' } + ); + }); + + it('testEndsWith', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(endsWith('title', 'y')) + .select('title') + .sort(field('title').descending()) + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'The Great Gatsby' } + ); + }); + + it('testStrContains', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(strContains('title', "'s")) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: "The Handmaid's Tale" }, + { title: "The Hitchhiker's Guide to the Galaxy" } + ); + }); + + it('testLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(charLength('title').as('titleLength'), field('title')) + .where(gt('titleLength', 20)) + .sort(field('title').ascending()) + ); + + expectResults( + snapshot, + + { + titleLength: 29, + title: 'One Hundred Years of Solitude' + }, + { + titleLength: 36, + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + titleLength: 21, + title: 'The Lord of the Rings' + }, + { + titleLength: 21, + title: 'To Kill a Mockingbird' + } + ); + }); + + it('testLike', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(like('title', '%Guide%')) + .select('title') + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('testRegexContains', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(regexContains('title', '(?i)(the|of)')) + ); + expect(snapshot.results.length).to.equal(5); + }); + + it('testRegexMatches', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(regexMatch('title', '.*(?i)(the|of).*')) + ); + expect(snapshot.results.length).to.equal(5); + }); + + it('testArithmeticOperations', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'To Kill a Mockingbird')) + .select( + add(field('rating'), 1).as('ratingPlusOne'), + subtract(field('published'), 1900).as('yearsSince1900'), + field('rating').multiply(10).as('ratingTimesTen'), + divide('rating', 2).as('ratingDividedByTwo'), + multiply('rating', 10, 2).as('ratingTimes20'), + add('rating', 1, 2).as('ratingPlus3'), + mod('rating', 2).as('ratingMod2') + ) + .limit(1) + ); + expectResults(snapshot, { + ratingPlusOne: 5.2, + yearsSince1900: 60, + ratingTimesTen: 42, + ratingDividedByTwo: 2.1, + ratingTimes20: 84, + ratingPlus3: 7.2, + ratingMod2: 0.20000000000000018 + }); + }); + + it('testComparisonOperators', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + gt('rating', 4.2), + lte(field('rating'), 4.5), + neq('genre', 'Science Fiction') + ) + ) + .select('rating', 'title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { rating: 4.3, title: 'Crime and Punishment' }, + { + rating: 4.3, + title: 'One Hundred Years of Solitude' + }, + { rating: 4.5, title: 'Pride and Prejudice' } + ); + }); + + it('testLogicalOperators', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + and(gt('rating', 4.5), eq('genre', 'Science Fiction')), + lt('published', 1900) + ) + ) + .select('title') + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: 'Crime and Punishment' }, + { title: 'Dune' }, + { title: 'Pride and Prejudice' } + ); + }); + + it.only('testChecks', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + isNull('rating').as('ratingIsNull'), + isNan('rating').as('ratingIsNaN'), + isError(arrayOffset('title', 0)).as('isError'), + ifError(arrayOffset('title', 0), constant('was error')).as( + 'ifError' + ), + isAbsent('foo').as('isAbsent'), + isNotNull('title').as('titleIsNotNull'), + isNotNan('cost').as('costIsNotNan'), + exists('fooBarBaz').as('fooBarBazExists'), + field('title').exists().as('titleExists') + ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false, + fooBarBazExists: false, + titleExists: true + }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + field('rating').isNull().as('ratingIsNull'), + field('rating').isNan().as('ratingIsNaN'), + arrayOffset('title', 0).isError().as('isError'), + arrayOffset('title', 0) + .ifError(constant('was error')) + .as('ifError'), + field('foo').isAbsent().as('isAbsent'), + field('title').isNotNull().as('titleIsNotNull'), + field('cost').isNotNan().as('costIsNotNan') + ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false + }); + }); + + it('testMapGet', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('published').descending()) + .select( + field('awards').mapGet('hugo').as('hugoAward'), + field('awards').mapGet('others').as('others'), + field('title') + ) + .where(eq('hugoAward', true)) + ); + expectResults( + snapshot, + { + hugoAward: true, + title: "The Hitchhiker's Guide to the Galaxy", + others: { unknown: { year: 1980 } } + }, + { hugoAward: true, title: 'Dune', others: null } + ); + }); + + it('testDistanceFunctions', async () => { + const sourceVector = [0.1, 0.1]; + const targetVector = [0.5, 0.8]; + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + cosineDistance(constantVector(sourceVector), targetVector).as( + 'cosineDistance' + ), + dotProduct(constantVector(sourceVector), targetVector).as( + 'dotProductDistance' + ), + euclideanDistance(constantVector(sourceVector), targetVector).as( + 'euclideanDistance' + ) + ) + .limit(1) + ); + + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855 + }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + constantVector(sourceVector) + .cosineDistance(targetVector) + .as('cosineDistance'), + constantVector(sourceVector) + .dotProduct(targetVector) + .as('dotProductDistance'), + constantVector(sourceVector) + .euclideanDistance(targetVector) + .as('euclideanDistance') + ) + .limit(1) + ); + + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855 + }); + }); + + it('testVectorLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + ); + expectResults(snapshot, { + vectorLength: 3 + }); + }); + + it('testNestedFields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('awards.hugo', true)) + .sort(descending('title')) + .select('title', 'awards.hugo') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + 'awards.hugo': true + }, + { title: 'Dune', 'awards.hugo': true } + ); + }); + + it('test mapGet with field name including . notation', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + ) + .sort(descending('title')) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + 'nestedField.level.`1`': null, + nested: true + }, + { title: 'Dune', 'nestedField.level.`1`': null, nested: null } + ); + }); + + describe('genericFunction', () => { + it('add selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(descending('rating')) + .limit(1) + .select( + new FunctionExpr('add', [field('rating'), constant(1)]).as( + 'rating' + ) + ) + ); + expectResults(snapshot, { + rating: 5.7 + }); + }); + + it('and (variadic) selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('and', [ + field('rating').gt(0), + field('title').charLength().lt(5), + field('tags').arrayContains('propaganda') + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: '1984' + }); + }); + + it('array contains any', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('array_contains_any', [ + field('tags'), + array(['politics']) + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: 'Dune' + }); + }); + + it('countif aggregate', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( + 'countOfBest' + ) + ) + ); + expectResults(snapshot, { + countOfBest: 3 + }); + }); + + it('sort by char_len', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort( + new FunctionExpr('char_length', [field('title')]).ascending(), + descending('__name__') + ) + .limit(3) + .select('title') + ); + expectResults( + snapshot, + { + title: '1984' + }, + { + title: 'Dune' + }, + { + title: 'The Great Gatsby' + } + ); + }); + }); + + itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'The Lord of the Rings')) + .limit(1) + .select(replaceFirst('title', 'o', '0').as('newName')) + ); + expectResults(snapshot, { newName: 'The L0rd of the Rings' }); + }); + + itIf(testUnsupportedFeatures)('testReplaceAll', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', 'The Lord of the Rings')) + .limit(1) + .select(replaceAll('title', 'o', '0').as('newName')) + ); + expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); + }); + + it('supports Rand', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(10) + .select(rand().as('result')) + ); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(d => { + expect(d.get('result')).to.be.lt(1); + expect(d.get('result')).to.be.gte(0); + }); + }); + + it('supports array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(array([1, 2, 3, 4]).as('metadata')) + ); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: [1, 2, 3, 4] + }); + }); + + it('evaluates expression in array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + array([1, 2, field('genre'), multiply('rating', 10)]).as('metadata') + ) + ); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: [1, 2, 'Fantasy', 47] + }); + }); + + it('supports arrayOffset', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(arrayOffset('tags', 0).as('firstTag')) + ); + const expectedResults = [ + { + firstTag: 'adventure' + }, + { + firstTag: 'politics' + }, + { + firstTag: 'classic' + } + ]; + expectResults(snapshot, ...expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(field('tags').arrayOffset(0).as('firstTag')) + ); + expectResults(snapshot, ...expectedResults); + }); + + it('supports map', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + map({ + foo: 'bar' + }).as('metadata') + ) + ); + + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + foo: 'bar' + } + }); + }); + + it('evaluates expression in map', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + map({ + genre: field('genre'), + rating: field('rating').multiply(10) + }).as('metadata') + ) + ); + + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + genre: 'Fantasy', + rating: 47 + } + }); + }); + + it('supports mapRemove', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(mapRemove('awards', 'hugo').as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false } + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('awards').mapRemove('hugo').as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false } + }); + }); + + it('supports mapMerge', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(mapMerge('awards', { fakeAward: true }).as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false, hugo: false, fakeAward: true } + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('awards').mapMerge({ fakeAward: true }).as('awards')) + ); + expectResults(snapshot, { + awards: { nebula: false, hugo: false, fakeAward: true } + }); + }); + + it('supports timestamp conversions', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + unixSecondsToTimestamp(constant(1741380235)).as( + 'unixSecondsToTimestamp' + ), + unixMillisToTimestamp(constant(1741380235123)).as( + 'unixMillisToTimestamp' + ), + unixMicrosToTimestamp(constant(1741380235123456)).as( + 'unixMicrosToTimestamp' + ), + timestampToUnixSeconds( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixSeconds'), + timestampToUnixMicros( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixMicros'), + timestampToUnixMillis( + constant(new Timestamp(1741380235, 123456789)) + ).as('timestampToUnixMillis') + ) + ); + expectResults(snapshot, { + unixMicrosToTimestamp: new Timestamp(1741380235, 123456000), + unixMillisToTimestamp: new Timestamp(1741380235, 123000000), + unixSecondsToTimestamp: new Timestamp(1741380235, 0), + timestampToUnixSeconds: 1741380235, + timestampToUnixMicros: 1741380235123456, + timestampToUnixMillis: 1741380235123 + }); + }); + + it('supports timestamp math', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(new Timestamp(1741380235, 0)).as('timestamp')) + .select( + timestampAdd('timestamp', 'day', 10).as('plus10days'), + timestampAdd('timestamp', 'hour', 10).as('plus10hours'), + timestampAdd('timestamp', 'minute', 10).as('plus10minutes'), + timestampAdd('timestamp', 'second', 10).as('plus10seconds'), + timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), + timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), + timestampSub('timestamp', 'day', 10).as('minus10days'), + timestampSub('timestamp', 'hour', 10).as('minus10hours'), + timestampSub('timestamp', 'minute', 10).as('minus10minutes'), + timestampSub('timestamp', 'second', 10).as('minus10seconds'), + timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), + timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + ) + ); + expectResults(snapshot, { + plus10days: new Timestamp(1742244235, 0), + plus10hours: new Timestamp(1741416235, 0), + plus10minutes: new Timestamp(1741380835, 0), + plus10seconds: new Timestamp(1741380245, 0), + plus10micros: new Timestamp(1741380235, 10000), + plus10millis: new Timestamp(1741380235, 10000000), + minus10days: new Timestamp(1740516235, 0), + minus10hours: new Timestamp(1741344235, 0), + minus10minutes: new Timestamp(1741379635, 0), + minus10seconds: new Timestamp(1741380225, 0), + minus10micros: new Timestamp(1741380234, 999990000), + minus10millis: new Timestamp(1741380234, 990000000) + }); + }); + + it('supports byteLength', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .select( + constant( + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) + ).as('bytes') + ) + .select(byteLength('bytes').as('byteLength')) + ); + + expectResults(snapshot, { + byteLength: 8 + }); + }); + + it('supports not', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .select(constant(true).as('trueField')) + .select('trueField', not(eq('trueField', true)).as('falseField')) + ); + + expectResults(snapshot, { + trueField: true, + falseField: false + }); + }); + }); + + describe('not yet implemented in backend', () => { + itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitAnd(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 4 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitAnd(12).as('result')) + ); + expectResults(snapshot, { + result: 4 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_or', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitOr(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 13 + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitOr(12).as('result')) + ); + expectResults(snapshot, { + result: 13 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitXor(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 9 + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitXor(12).as('result')) + ); + expectResults(snapshot, { + result: 9 + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_not', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( + 'result' + ) + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) + .bitNot() + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitLeftShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitLeftShift(2) + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitRightShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitRightShift(2) + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + }); + + itIf(testUnsupportedFeatures)('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(documentId(field('__path__')).as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('__path__').documentId().as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + }); + + itIf(testUnsupportedFeatures)('supports Substr', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); + + itIf(testUnsupportedFeatures)( + 'supports Substr without length', + async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + } + ); + + itIf(testUnsupportedFeatures)('arrayConcat works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + arrayConcat('tags', ['newTag1', 'newTag2'], field('tags'), [ + null + ]).as('modifiedTags') + ) + .limit(1) + ); + expectResults(snapshot, { + modifiedTags: [ + 'comedy', + 'space', + 'adventure', + 'newTag1', + 'newTag2', + 'comedy', + 'space', + 'adventure', + null + ] + }); + }); + + itIf(testUnsupportedFeatures)('testToLowercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(toLower('title').as('lowercaseTitle')) + .limit(1) + ); + expectResults(snapshot, { + lowercaseTitle: "the hitchhiker's guide to the galaxy" + }); + }); + + itIf(testUnsupportedFeatures)('testToUppercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(toUpper('author').as('uppercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + }); + + itIf(testUnsupportedFeatures)('testTrim', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + constant(" The Hitchhiker's Guide to the Galaxy ").as('spacedTitle') + ) + .select(trim('spacedTitle').as('trimmedTitle'), field('spacedTitle')) + .limit(1) + ); + expectResults(snapshot, { + spacedTitle: " The Hitchhiker's Guide to the Galaxy ", + trimmedTitle: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + itIf(testUnsupportedFeatures)('test reverse', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', '1984')) + .limit(1) + .select(reverse('title').as('reverseTitle')) + ); + expectResults(snapshot, { title: '4891' }); + }); + }); + + describe('pagination', () => { + /** + * Adds several books to the test collection. These + * additional books support pagination test scenarios + * that would otherwise not be possible with the original + * set of books. + * @param collectionReference + */ + async function addBooks( + collectionReference: CollectionReference + ): Promise { + await setDoc(doc(collectionReference, 'book11'), { + title: 'Jonathan Strange & Mr Norrell', + author: 'Susanna Clarke', + genre: 'Fantasy', + published: 2004, + rating: 4.6, + tags: ['historical fantasy', 'magic', 'alternate history', 'england'], + awards: { hugo: false, nebula: false } + }); + await setDoc(doc(collectionReference, 'book12'), { + title: 'The Master and Margarita', + author: 'Mikhail Bulgakov', + genre: 'Satire', + published: 1967, // Though written much earlier + rating: 4.6, + tags: [ + 'russian literature', + 'supernatural', + 'philosophy', + 'dark comedy' + ], + awards: {} + }); + await setDoc(doc(collectionReference, 'book13'), { + title: 'A Long Way to a Small, Angry Planet', + author: 'Becky Chambers', + genre: 'Science Fiction', + published: 2014, + rating: 4.6, + tags: ['space opera', 'found family', 'character-driven', 'optimistic'], + awards: { hugo: false, nebula: false, kitschies: true } + }); + } + + // sort on __name__ is not working + itIf(testUnsupportedFeatures)( + 'supports pagination with filters', + async () => { + await addBooks(randomCol); + const pageSize = 2; + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', '__name__') + .sort(field('rating').descending(), field('__name__').ascending()); + + let snapshot = await execute(pipeline.limit(pageSize)); + expectResults( + snapshot, + { title: 'The Lord of the Rings', rating: 4.7 }, + { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 } + ); + + const lastDoc = snapshot.results[snapshot.results.length - 1]; + + snapshot = await execute( + pipeline + .where( + or( + and( + field('rating').eq(lastDoc.get('rating')), + field('__path__').gt(lastDoc.ref?.id) + ), + field('rating').lt(lastDoc.get('rating')) + ) + ) + .limit(pageSize) + ); + expectResults( + snapshot, + { title: 'Pride and Prejudice', rating: 4.5 }, + { title: 'Crime and Punishment', rating: 4.3 } + ); + } + ); + + // sort on __name__ is not working + itIf(testUnsupportedFeatures)( + 'supports pagination with offsets', + async () => { + await addBooks(randomCol); + + const secondFilterField = '__path__'; + + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', secondFilterField) + .sort( + field('rating').descending(), + field(secondFilterField).ascending() + ); + + const pageSize = 2; + let currPage = 0; + + let snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + + expectResults( + snapshot, + { + title: 'The Lord of the Rings', + rating: 4.7 + }, + { title: 'Dune', rating: 4.6 } + ); + + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'Jonathan Strange & Mr Norrell', + rating: 4.6 + }, + { title: 'The Master and Margarita', rating: 4.6 } + ); + + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 + }, + { + title: 'Pride and Prejudice', + rating: 4.5 + } + ); + } + ); + }); +}); diff --git a/packages/firestore/test/unit/api/document_change.test.ts b/packages/firestore/test/unit/api/document_change.test.ts index faae8b4d4c8..8ce40f599b8 100644 --- a/packages/firestore/test/unit/api/document_change.test.ts +++ b/packages/firestore/test/unit/api/document_change.test.ts @@ -18,8 +18,8 @@ import { expect } from 'chai'; import { Query } from '../../../src/api/reference'; -import { ExpUserDataWriter } from '../../../src/api/reference_impl'; import { QuerySnapshot } from '../../../src/api/snapshot'; +import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; import { Query as InternalQuery } from '../../../src/core/query'; import { View } from '../../../src/core/view'; import { documentKeySet } from '../../../src/model/collections'; diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index d523c8fab83..451f7ddf7ae 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -28,7 +28,7 @@ import { serverTimestamp, Timestamp } from '../../../src'; -import { ExpUserDataWriter } from '../../../src/api/reference_impl'; +import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; import { DatabaseId } from '../../../src/core/database_info'; import { ArrayContainsAnyFilter, diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index 3e52c5873b9..afc6791dbd5 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -16,7 +16,7 @@ */ import { IndexConfiguration } from '../../../src/api/index_configuration'; -import { ExpUserDataWriter } from '../../../src/api/reference_impl'; +import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; import { ListenOptions, ListenerDataSource as Source diff --git a/packages/firestore/test/util/api_helpers.ts b/packages/firestore/test/util/api_helpers.ts index d248c9213b5..dc66a70a85b 100644 --- a/packages/firestore/test/util/api_helpers.ts +++ b/packages/firestore/test/util/api_helpers.ts @@ -32,7 +32,7 @@ import { EmptyAppCheckTokenProvider, EmptyAuthCredentialsProvider } from '../../src/api/credentials'; -import { ExpUserDataWriter } from '../../src/api/reference_impl'; +import { ExpUserDataWriter } from '../../src/api/user_data_writer'; import { DatabaseId } from '../../src/core/database_info'; import { newQueryForPath, Query as InternalQuery } from '../../src/core/query'; import { diff --git a/repo-scripts/prune-dts/extract-public-api.ts b/repo-scripts/prune-dts/extract-public-api.ts index c7517399565..4e3c0c3c1fc 100644 --- a/repo-scripts/prune-dts/extract-public-api.ts +++ b/repo-scripts/prune-dts/extract-public-api.ts @@ -141,7 +141,8 @@ export async function generateApi( typescriptDtsPath: string, rollupDtsPath: string, untrimmedRollupDtsPath: string, - publicDtsPath: string + publicDtsPath: string, + otherExportDtsPaths: string[] ): Promise { console.log(`Configuring API Extractor for ${packageName}`); writeTypeScriptConfig(packageRoot); @@ -160,7 +161,7 @@ export async function generateApi( }); console.log('Generated rollup DTS'); - pruneDts(rollupDtsPath, publicDtsPath); + pruneDts(rollupDtsPath, publicDtsPath, otherExportDtsPaths); console.log('Pruned DTS file'); await addBlankLines(publicDtsPath); console.log('Added blank lines after imports'); @@ -221,6 +222,13 @@ const argv = yargs 'The output file for the customer-facing .d.ts file that only ' + 'includes the public APIs', require: true + }, + otherExportsPublicDtsFiles: { + type: 'string', + desc: + 'Optional. A comma-separated list of customer-facing of .d.ts' + + 'files for other exports from this package.', + require: false } }) .parseSync(); @@ -231,5 +239,10 @@ void generateApi( path.resolve(argv.typescriptDts), path.resolve(argv.rollupDts), path.resolve(argv.untrimmedRollupDts), - path.resolve(argv.publicDts) + path.resolve(argv.publicDts), + argv.otherExportsPublicDtsFiles + ? argv.otherExportsPublicDtsFiles + .split(',') + .map(filePath => path.resolve(filePath)) + : [] ); diff --git a/repo-scripts/prune-dts/prune-dts.ts b/repo-scripts/prune-dts/prune-dts.ts index 70cfc2933bb..087d12a3d4b 100644 --- a/repo-scripts/prune-dts/prune-dts.ts +++ b/repo-scripts/prune-dts/prune-dts.ts @@ -18,6 +18,7 @@ import * as yargs from 'yargs'; import * as ts from 'typescript'; import * as fs from 'fs'; +import * as path from 'path'; import { ESLint } from 'eslint'; /** @@ -33,16 +34,32 @@ import { ESLint } from 'eslint'; * @param inputLocation The file path to the .d.ts produced by API explorer. * @param outputLocation The output location for the pruned .d.ts file. */ -export function pruneDts(inputLocation: string, outputLocation: string): void { +export function pruneDts( + inputLocation: string, + outputLocation: string, + otherExportFileLocations: string[] = [] +): void { const compilerOptions = {}; const host = ts.createCompilerHost(compilerOptions); - const program = ts.createProgram([inputLocation], compilerOptions, host); + const program = ts.createProgram( + [inputLocation, ...otherExportFileLocations], + compilerOptions, + host + ); const printer: ts.Printer = ts.createPrinter(); const sourceFile = program.getSourceFile(inputLocation)!; + const otherExportSourceFiles = otherExportFileLocations + .map(otherFileLocation => program.getSourceFile(otherFileLocation)) + .filter(value => value !== undefined) as ts.SourceFile[]; const result: ts.TransformationResult = ts.transform(sourceFile, [ - dropPrivateApiTransformer.bind(null, program, host) + dropPrivateApiTransformer.bind( + null, + program, + host, + otherExportSourceFiles + ) ]); const transformedSourceFile: ts.SourceFile = result.transformed[0]; let content = printer.printFile(transformedSourceFile); @@ -503,14 +520,59 @@ function extractExportedSymbol( return undefined; } +function findExternalExport( + typeChecker: ts.TypeChecker, + sourceFile: ts.SourceFile, + node: + | ts.InterfaceDeclaration + | ts.ClassDeclaration + | ts.TypeAliasDeclaration + | ts.EnumDeclaration, + otherExportSourceFiles: ts.SourceFile[] +): ts.SourceFile | undefined { + if (!node.name) return undefined; + + const localSymbolName = node.name.text; + + for (const otherExportSourceFile of otherExportSourceFiles) { + const otherExportedSymbols = typeChecker.getExportsOfModule( + typeChecker.getSymbolAtLocation(otherExportSourceFile)! + ); + + for (const symbol of otherExportedSymbols) { + // TODO: ideally this would compare definitions to handle the case + // of name collisions with different definitions. However, this + // implementation currently does not handle function exports, + // which is the only place we expect name collisions. + if (symbol.name === localSymbolName) { + return otherExportSourceFile; + } + } + } + + return undefined; +} + function dropPrivateApiTransformer( program: ts.Program, host: ts.CompilerHost, + otherExportSourceFiles: ts.SourceFile[], context: ts.TransformationContext ): ts.Transformer { const typeChecker = program.getTypeChecker(); return (sourceFile: ts.SourceFile) => { + const imports: Record> = {}; + + function ensureImportsForFile(filename: string): Array { + let importsForFile = imports[filename]; + if (!importsForFile) { + importsForFile = []; + imports[filename] = importsForFile; + } + return importsForFile; + } + function visit(node: ts.Node): ts.Node { if ( ts.isInterfaceDeclaration(node) || @@ -531,6 +593,30 @@ function dropPrivateApiTransformer( } } + if ( + ts.isInterfaceDeclaration(node) || + ts.isClassDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) + ) { + // Remove any types that are exported externally + const externalExportFile = findExternalExport( + typeChecker, + sourceFile, + node, + otherExportSourceFiles + ); + if (externalExportFile && node.name) { + ensureImportsForFile( + path.relative( + path.dirname(sourceFile.fileName), + externalExportFile.fileName + ) + ).push(node.name.text); + return ts.factory.createNotEmittedStatement(node); + } + } + if (ts.isConstructorDeclaration(node)) { // Replace internal constructors with private constructors. return maybeHideConstructor(node); @@ -580,7 +666,40 @@ function dropPrivateApiTransformer( context ) as T; } - return visitNodeAndChildren(sourceFile); + const result = visitNodeAndChildren(sourceFile); + + const moreImports: ts.ImportDeclaration[] = []; + for (let filename in imports) { + const importSpecifiers: ts.ImportSpecifier[] = []; + for (let identifier of imports[filename]) { + importSpecifiers.push( + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(identifier) + ) + ); + } + let outFileName = filename.startsWith('.') ? filename : `./${filename}`; + outFileName = outFileName.replace('.d.ts', ''); + const importDeclaration = ts.factory.createImportDeclaration( + [], + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports(importSpecifiers) + ), + ts.factory.createStringLiteral(outFileName, true) + ); + + moreImports.push(importDeclaration); + } + + return ts.factory.updateSourceFile( + result, + [...moreImports, ...result.statements], + true + ); }; } diff --git a/repo-scripts/size-analysis/bundle-definitions/firestore.json b/repo-scripts/size-analysis/bundle-definitions/firestore.json index f5ddafd167c..f265521b08b 100644 --- a/repo-scripts/size-analysis/bundle-definitions/firestore.json +++ b/repo-scripts/size-analysis/bundle-definitions/firestore.json @@ -128,6 +128,103 @@ } ] }, + { + "name": "Pipeline Query with lt filter (execute)", + "dependencies": [ + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "app", + "imports": [ + "initializeApp" + ] + } + ] + }, + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "firestore", + "imports": [ + "getFirestore", + "lt", + "Field", + "execute" + ] + } + ] + } + ] + }, + { + "name": "Pipeline Query with lt filter (useFirestorePipelines)", + "dependencies": [ + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "app", + "imports": [ + "initializeApp" + ] + } + ] + }, + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "firestore", + "imports": [ + "getFirestore", + "lt", + "Field", + "useFirestorePipelines" + ] + } + ] + } + ] + }, + { + "name": "Pipeline Query with lt plus and function", + "dependencies": [ + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "app", + "imports": [ + "initializeApp" + ] + } + ] + }, + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "firestore", + "imports": [ + "getFirestore", + "lt", + "Field", + "useFirestorePipelines", + "andFunction" + ] + } + ] + } + ] + }, { "name": "Query Cursors", "dependencies": [