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..37882d2eb50 --- /dev/null +++ b/common/api-review/firestore-lite-pipelines.api.md @@ -0,0 +1,1208 @@ +## 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: Expression, second: Expression | unknown): FunctionExpression; + +// @beta +export function add(fieldName: string, second: Expression | unknown): FunctionExpression; + +// @public +export type AddFieldsStageOptions = StageOptions & { + fields: Selectable[]; +}; + +// @beta +export class AggregateFunction { + constructor(name: string, params: Expression[]); + as(name: string): AliasedAggregate; + // (undocumented) + exprType: ExpressionType; + } + +// @public +export type AggregateStageOptions = StageOptions & { + accumulators: AliasedAggregate[]; + groups?: Array; +}; + +// @beta +export class AliasedAggregate { + constructor(aggregate: AggregateFunction, alias: string, _methodName: string | undefined); + // (undocumented) + readonly aggregate: AggregateFunction; + // (undocumented) + readonly alias: string; +} + +// @beta (undocumented) +export class AliasedExpression implements Selectable { + constructor(expr: Expression, alias: string, _methodName: string | undefined); + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expression; + // (undocumented) + exprType: ExpressionType; + // (undocumented) + selectable: true; +} + +// @beta +export function and(first: BooleanExpression, second: BooleanExpression, ...more: BooleanExpression[]): BooleanExpression; + +// @beta +export function array(elements: unknown[]): FunctionExpression; + +// @beta +export function arrayConcat(firstArray: Expression, secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + +// @beta +export function arrayConcat(firstArrayField: string, secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + +// @beta +export function arrayContains(array: Expression, element: Expression): FunctionExpression; + +// @beta +export function arrayContains(array: Expression, element: unknown): FunctionExpression; + +// @beta +export function arrayContains(fieldName: string, element: Expression): FunctionExpression; + +// @beta +export function arrayContains(fieldName: string, element: unknown): BooleanExpression; + +// @beta +export function arrayContainsAll(array: Expression, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAll(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAll(array: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function arrayContainsAll(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// @beta +export function arrayContainsAny(array: Expression, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAny(array: Expression, values: Expression): BooleanExpression; + +// @beta +export function arrayContainsAny(fieldName: string, values: Expression): BooleanExpression; + +// @beta +export function arrayLength(fieldName: string): FunctionExpression; + +// @beta +export function arrayLength(array: Expression): FunctionExpression; + +// @beta +export function ascending(expr: Expression): Ordering; + +// @beta +export function ascending(fieldName: string): Ordering; + +// @beta +export function average(expression: Expression): AggregateFunction; + +// @beta +export function average(fieldName: string): AggregateFunction; + +// @beta +export class BooleanExpression extends FunctionExpression { + conditional(thenExpr: Expression, elseExpr: Expression): FunctionExpression; + countIf(): AggregateFunction; + // (undocumented) + filterable: true; + ifError(catchValue: BooleanExpression): BooleanExpression; + not(): BooleanExpression; +} + +// @beta +export function byteLength(expr: Expression): FunctionExpression; + +// @beta +export function byteLength(fieldName: string): FunctionExpression; + +// @beta +export function charLength(fieldName: string): FunctionExpression; + +// @beta +export function charLength(stringExpression: Expression): FunctionExpression; + +// @public +export type CollectionGroupStageOptions = StageOptions & { + collectionId: string; + forceIndex?: string; +}; + +// @public +export type CollectionStageOptions = StageOptions & { + collection: string | Query; + forceIndex?: string; +}; + +// @beta +export function conditional(condition: BooleanExpression, thenExpr: Expression, elseExpr: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: number): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: string): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// +// @public +export function constant(value: boolean): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: null): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: GeoPoint): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: Timestamp): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: Date): Expression; + +// Warning: (ae-forgotten-export) The symbol "Bytes" 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: Bytes): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: DocumentReference): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: VectorValue): Expression; + +// @beta +export function cosineDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function cosineDistance(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function cosineDistance(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function cosineDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// @beta +export function count(expression: Expression): 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: BooleanExpression): AggregateFunction; + +// @public +export type DatabaseStageOptions = StageOptions & {}; + +// @beta +export function descending(expr: Expression): Ordering; + +// @beta +export function descending(fieldName: string): Ordering; + +// @public +export type DistinctStageOptions = StageOptions & { + groups: Array; +}; + +// @beta +export function divide(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function divide(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function divide(fieldName: string, expressions: Expression): FunctionExpression; + +// @beta +export function divide(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function documentId(documentPath: string | DocumentReference): FunctionExpression; + +// @beta +export function documentId(documentPathExpr: Expression): FunctionExpression; + +// @public +export type DocumentsStageOptions = StageOptions & { + docs: Array; +}; + +// @beta +export function dotProduct(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function dotProduct(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function dotProduct(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function dotProduct(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// @beta +export function endsWith(fieldName: string, suffix: string): BooleanExpression; + +// @beta +export function endsWith(fieldName: string, suffix: Expression): BooleanExpression; + +// @beta +export function endsWith(stringExpression: Expression, suffix: string): BooleanExpression; + +// @beta +export function endsWith(stringExpression: Expression, suffix: Expression): BooleanExpression; + +// @beta +export function equal(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function equal(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function equal(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function equal(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function equalAny(expression: Expression, values: Array): BooleanExpression; + +// @beta +export function equalAny(expression: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function equalAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function equalAny(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// @beta +export function euclideanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function euclideanDistance(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function euclideanDistance(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function euclideanDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "execute" is marked as @public, but its signature references "Pipeline" which is marked as @beta +// +// @public +export function execute(pipeline: Pipeline): Promise; + +// @beta +export function exists(value: Expression): BooleanExpression; + +// @beta +export function exists(fieldName: string): BooleanExpression; + +// @beta +export abstract class Expression { + abs(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + add(second: Expression | unknown): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayConcat(secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayContains(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContains(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayGet(offset: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayGet(offsetExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayReverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arraySum(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + as(name: string): AliasedExpression; + /* Excluded from this release type: _readUserData */ + ascending(): Ordering; + /* Excluded from this release type: _readUserData */ + average(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + byteLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + ceil(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + charLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + collectionId(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + concat(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + cosineDistance(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + cosineDistance(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + count(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + countDistinct(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + descending(): Ordering; + /* Excluded from this release type: _readUserData */ + divide(divisor: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + divide(divisor: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + documentId(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + dotProduct(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + dotProduct(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equal(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equal(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equalAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equalAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + exists(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + exp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + // (undocumented) + abstract readonly expressionType: ExpressionType; + /* Excluded from this release type: _readUserData */ + floor(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + greaterThan(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThan(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThanOrEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThanOrEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + ifAbsent(elseValue: unknown): Expression; + /* Excluded from this release type: _readUserData */ + ifAbsent(elseExpression: unknown): Expression; + /* Excluded from this release type: _readUserData */ + ifError(catchExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + ifError(catchValue: unknown): FunctionExpression; + /* Excluded from this release type: _readUserData */ + isAbsent(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isError(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNan(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNotNan(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNotNull(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNull(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + join(delimiterExpression: Expression): Expression; + /* Excluded from this release type: _readUserData */ + join(delimiter: string): Expression; + /* Excluded from this release type: _readUserData */ + length(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + lessThan(experession: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThan(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThanOrEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThanOrEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + like(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + like(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + ln(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + log10(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + logicalMaximum(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + logicalMinimum(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapGet(subfield: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapMerge(secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapRemove(key: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapRemove(keyExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + maximum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + minimum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + mod(expression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mod(value: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + multiply(second: Expression | number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + notEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqualAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqualAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + pow(exponent: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + pow(exponent: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + reverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(decimalPlaces: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(decimalPlaces: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + sqrt(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringConcat(secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + stringContains(substring: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringContains(expr: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringReverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + substring(position: number, length?: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + substring(position: Expression, length?: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + subtract(subtrahend: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + subtract(subtrahend: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + sum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: Expression, amount: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampSubtract(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixMicros(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixMillis(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixSeconds(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + toLower(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + toUpper(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + trim(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixMicrosToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixMillisToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixSecondsToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + vectorLength(): FunctionExpression; +} + +// @beta +export type ExpressionType = 'Field' | 'Constant' | 'Function' | 'AggregateFunction' | 'ListOfExpressions' | 'AliasedExpression'; + +// @beta +export class Field extends Expression implements Selectable { + // (undocumented) + get alias(): string; + // (undocumented) + get expr(): Expression; + // (undocumented) + readonly expressionType: ExpressionType; + // (undocumented) + get 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; + +// @public +export type FindNearestStageOptions = StageOptions & { + field: Field | string; + vectorValue: VectorValue | number[]; + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + limit?: number; + distanceField?: string; +}; + +// @beta +export class FunctionExpression extends Expression { + constructor(name: string, params: Expression[]); + constructor(name: string, params: Expression[], _methodName: string | undefined); + // (undocumented) + readonly expressionType: ExpressionType; + } + +// @beta +export function greaterThan(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function greaterThan(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function greaterThan(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function greaterThan(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function greaterThanOrEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function greaterThanOrEqual(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function greaterThanOrEqual(fieldName: string, value: Expression): BooleanExpression; + +// @beta +export function greaterThanOrEqual(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function ifError(tryExpr: BooleanExpression, catchExpr: BooleanExpression): BooleanExpression; + +// @beta +export function ifError(tryExpr: Expression, catchExpr: Expression): FunctionExpression; + +// @beta +export function ifError(tryExpr: Expression, catchValue: unknown): FunctionExpression; + +// @beta +export function isAbsent(value: Expression): BooleanExpression; + +// @beta +export function isAbsent(field: string): BooleanExpression; + +// @beta +export function isError(value: Expression): BooleanExpression; + +// @beta +export function isNan(value: Expression): BooleanExpression; + +// @beta +export function isNan(fieldName: string): BooleanExpression; + +// @beta +export function isNotNan(value: Expression): BooleanExpression; + +// @beta +export function isNotNan(value: string): BooleanExpression; + +// @beta +export function isNotNull(value: Expression): BooleanExpression; + +// @beta +export function isNotNull(value: string): BooleanExpression; + +// @beta +export function isNull(value: Expression): BooleanExpression; + +// @beta +export function isNull(value: string): BooleanExpression; + +// @beta +export function lessThan(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function lessThan(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function lessThan(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function lessThan(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function lessThanOrEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function lessThanOrEqual(expression: Expression, value: unknown): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// +// @public +export function lessThanOrEqual(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function lessThanOrEqual(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function like(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function like(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function like(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function like(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @public +export type LimitStageOptions = StageOptions & { + limit: number; +}; + +// @beta +export function logicalMaximum(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMaximum(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMinimum(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMinimum(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function map(elements: Record): FunctionExpression; + +// @beta +export function mapGet(fieldName: string, subField: string): FunctionExpression; + +// @beta +export function mapGet(mapExpression: Expression, subField: string): FunctionExpression; + +// @beta +export function mapMerge(mapField: string, secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + +// @beta +export function mapMerge(firstMap: Record | Expression, secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + +// @beta +export function mapRemove(mapField: string, key: string): FunctionExpression; + +// @beta +export function mapRemove(mapExpr: Expression, key: string): FunctionExpression; + +// @beta +export function mapRemove(mapField: string, keyExpr: Expression): FunctionExpression; + +// @beta +export function mapRemove(mapExpr: Expression, keyExpr: Expression): FunctionExpression; + +// @beta +export function maximum(expression: Expression): AggregateFunction; + +// @beta +export function maximum(fieldName: string): AggregateFunction; + +// @beta +export function minimum(expression: Expression): AggregateFunction; + +// @beta +export function minimum(fieldName: string): AggregateFunction; + +// @beta +export function mod(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function mod(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function mod(fieldName: string, expression: Expression): FunctionExpression; + +// @beta +export function mod(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function multiply(first: Expression, second: Expression | unknown): FunctionExpression; + +// @beta +export function multiply(fieldName: string, second: Expression | unknown): FunctionExpression; + +// @beta +export function not(booleanExpr: BooleanExpression): BooleanExpression; + +// @beta +export function notEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function notEqual(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function notEqual(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function notEqual(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function notEqualAny(element: Expression, values: Array): BooleanExpression; + +// @beta +export function notEqualAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function notEqualAny(element: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function notEqualAny(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// @public +export type OffsetStageOptions = StageOptions & { + offset: number; +}; + +// @public +export type OneOf = { + [K in keyof T]: Pick & { + [P in Exclude]?: undefined; + }; +}[keyof T]; + +// @beta +export function or(first: BooleanExpression, second: BooleanExpression, ...more: BooleanExpression[]): BooleanExpression; + +// @beta +export class Ordering { + constructor(expr: Expression, direction: 'ascending' | 'descending', _methodName: string | undefined); + // (undocumented) + readonly direction: 'ascending' | 'descending'; + // (undocumented) + readonly expr: Expression; +} + +// @beta +export class Pipeline { + /* Excluded from this release type: _db */ + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + addFields(options: AddFieldsStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + aggregate(accumulator: AliasedAggregate, ...additionalAccumulators: AliasedAggregate[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + aggregate(options: AggregateStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + distinct(group: string | Selectable, ...additionalGroups: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + distinct(options: DistinctStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + findNearest(options: FindNearestStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + limit(limit: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + limit(options: LimitStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + offset(offset: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + offset(options: OffsetStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + rawStage(name: string, params: unknown[], options?: { + [key: string]: Expression | unknown; + }): Pipeline; + /* Excluded from this release type: _userDataWriter */ + removeFields(fieldValue: Field | string, ...additionalFields: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + removeFields(options: RemoveFieldsStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + replaceWith(fieldName: string): Pipeline; + /* Excluded from this release type: _userDataWriter */ + replaceWith(expr: Expression): Pipeline; + /* Excluded from this release type: _userDataWriter */ + replaceWith(options: ReplaceWithStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sample(documents: number): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sample(options: SampleStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + select(selection: Selectable | string, ...additionalSelections: Array): Pipeline; + /* Excluded from this release type: _userDataWriter */ + select(options: SelectStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; + /* Excluded from this release type: _userDataWriter */ + sort(options: SortStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + union(other: Pipeline): Pipeline; + /* Excluded from this release type: _userDataWriter */ + union(options: UnionStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + unnest(selectable: Selectable, indexField?: string): Pipeline; + /* Excluded from this release type: _userDataWriter */ + unnest(options: UnnestStageOptions): Pipeline; + /* Excluded from this release type: _userDataWriter */ + where(condition: BooleanExpression): Pipeline; + /* Excluded from this release type: _userDataWriter */ + where(options: WhereStageOptions): 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; + 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 "Pipeline" which is marked as @beta + // 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; + // 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(collection: string | Query): PipelineType; + collection(options: CollectionStageOptions): PipelineType; + collectionGroup(collectionId: string): PipelineType; + collectionGroup(options: CollectionGroupStageOptions): PipelineType; + createFrom(query: Query): Pipeline; + database(): PipelineType; + database(options: DatabaseStageOptions): PipelineType; + documents(docs: Array): PipelineType; + documents(options: DocumentsStageOptions): PipelineType; + } + +// @beta +export function regexContains(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function regexContains(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function regexContains(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function regexContains(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @beta +export function regexMatch(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function regexMatch(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function regexMatch(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function regexMatch(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @public +export type RemoveFieldsStageOptions = StageOptions & { + fields: Array; +}; + +// @public +export type ReplaceWithStageOptions = StageOptions & { + map: Expression | string; +}; + +// @beta +export function reverse(stringExpression: Expression): FunctionExpression; + +// @beta +export function reverse(field: string): FunctionExpression; + +// @public +export type SampleStageOptions = StageOptions & OneOf<{ + percentage: number; + documents: number; +}>; + +// @beta +export interface Selectable { + // (undocumented) + selectable: true; +} + +// @public +export type SelectStageOptions = StageOptions & { + selections: Array; +}; + +// @public +export type SortStageOptions = StageOptions & { + orderings: Ordering[]; +}; + +// @public +export interface StageOptions { + rawOptions?: { + [name: string]: unknown; + }; +} + +// @beta +export function startsWith(fieldName: string, prefix: string): BooleanExpression; + +// @beta +export function startsWith(fieldName: string, prefix: Expression): BooleanExpression; + +// @beta +export function startsWith(stringExpression: Expression, prefix: string): BooleanExpression; + +// @beta +export function startsWith(stringExpression: Expression, prefix: Expression): BooleanExpression; + +// @beta +export function stringConcat(fieldName: string, secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + +// @beta +export function stringConcat(firstString: Expression, secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + +// @beta +export function stringContains(fieldName: string, substring: string): BooleanExpression; + +// @beta +export function stringContains(fieldName: string, substring: Expression): BooleanExpression; + +// @beta +export function stringContains(stringExpression: Expression, substring: string): BooleanExpression; + +// @beta +export function stringContains(stringExpression: Expression, substring: Expression): BooleanExpression; + +// @beta +export function substring(field: string, position: number, length?: number): FunctionExpression; + +// @beta +export function substring(input: Expression, position: number, length?: number): FunctionExpression; + +// @beta +export function substring(field: string, position: Expression, length?: Expression): FunctionExpression; + +// @beta +export function substring(input: Expression, position: Expression, length?: Expression): FunctionExpression; + +// @beta +export function subtract(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function subtract(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function subtract(fieldName: string, expression: Expression): FunctionExpression; + +// @beta +export function subtract(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function timestampAdd(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; + +// @beta +export function timestampAdd(timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampAdd(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampSubtract(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; + +// @beta +export function timestampSubtract(timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampSubtract(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampToUnixMicros(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixMicros(fieldName: string): FunctionExpression; + +// @beta +export function timestampToUnixMillis(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixMillis(fieldName: string): FunctionExpression; + +// @beta +export function timestampToUnixSeconds(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixSeconds(fieldName: string): FunctionExpression; + +// @beta +export function toLower(fieldName: string): FunctionExpression; + +// @beta +export function toLower(stringExpression: Expression): FunctionExpression; + +// @beta +export function toUpper(fieldName: string): FunctionExpression; + +// @beta +export function toUpper(stringExpression: Expression): FunctionExpression; + +// @beta +export function trim(fieldName: string): FunctionExpression; + +// @beta +export function trim(stringExpression: Expression): FunctionExpression; + +// @public +export type UnionStageOptions = StageOptions & { + other: Pipeline; +}; + +// @beta +export function unixMicrosToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixMicrosToTimestamp(fieldName: string): FunctionExpression; + +// @beta +export function unixMillisToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixMillisToTimestamp(fieldName: string): FunctionExpression; + +// @beta +export function unixSecondsToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixSecondsToTimestamp(fieldName: string): FunctionExpression; + +// @public +export type UnnestStageOptions = StageOptions & { + selectable: Selectable; + indexField?: string; +}; + +// @beta +export function vectorLength(vectorExpression: Expression): FunctionExpression; + +// @beta +export function vectorLength(fieldName: string): FunctionExpression; + +// @public +export type WhereStageOptions = StageOptions & { + condition: BooleanExpression; +}; + +// @beta +export function xor(first: BooleanExpression, second: BooleanExpression, ...additionalConditions: BooleanExpression[]): BooleanExpression; + + +// Warnings were encountered during analysis: +// +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:55:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:92:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:97:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:606:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:862:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:2871:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5095:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5105:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5175:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5185:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5734:5 - (ae-incompatible-release-tags) The symbol "other" is marked as @public, but its signature references "Pipeline" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5834:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5876:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" 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..add73ae322f --- /dev/null +++ b/common/api-review/firestore-pipelines.api.md @@ -0,0 +1,1567 @@ +## 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'; + +// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function abs(expr: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function abs(fieldName: string): FunctionExpression; + +// @beta +export function add(first: Expression, second: Expression | unknown): FunctionExpression; + +// @beta +export function add(fieldName: string, second: Expression | unknown): FunctionExpression; + +// @beta (undocumented) +export class AddFields extends Stage { + constructor(fields: Map, options: StageOptions); + } + +// @public +export type AddFieldsStageOptions = StageOptions & { + fields: Selectable[]; +}; + +// @beta (undocumented) +export class Aggregate extends Stage { + constructor(groups: Map, accumulators: Map, options: StageOptions); + } + +// @beta +export class AggregateFunction { + constructor(name: string, params: Expression[]); + as(name: string): AliasedAggregate; + // (undocumented) + exprType: ExpressionType; + } + +// @public +export type AggregateStageOptions = StageOptions & { + accumulators: AliasedAggregate[]; + groups?: Array; +}; + +// @beta +export class AliasedAggregate { + constructor(aggregate: AggregateFunction, alias: string, _methodName: string | undefined); + // (undocumented) + readonly aggregate: AggregateFunction; + // (undocumented) + readonly alias: string; +} + +// @beta (undocumented) +export class AliasedExpression implements Selectable { + constructor(expr: Expression, alias: string, _methodName: string | undefined); + // (undocumented) + readonly alias: string; + // (undocumented) + readonly expr: Expression; + // (undocumented) + exprType: ExpressionType; + // (undocumented) + selectable: true; +} + +// @beta +export function and(first: BooleanExpression, second: BooleanExpression, ...more: BooleanExpression[]): BooleanExpression; + +// @beta +export function array(elements: unknown[]): FunctionExpression; + +// @beta +export function arrayConcat(firstArray: Expression, secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + +// @beta +export function arrayConcat(firstArrayField: string, secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + +// @beta +export function arrayContains(array: Expression, element: Expression): FunctionExpression; + +// @beta +export function arrayContains(array: Expression, element: unknown): FunctionExpression; + +// @beta +export function arrayContains(fieldName: string, element: Expression): FunctionExpression; + +// @beta +export function arrayContains(fieldName: string, element: unknown): BooleanExpression; + +// @beta +export function arrayContainsAll(array: Expression, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAll(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAll(array: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function arrayContainsAll(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// @beta +export function arrayContainsAny(array: Expression, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function arrayContainsAny(array: Expression, values: Expression): BooleanExpression; + +// @beta +export function arrayContainsAny(fieldName: string, values: Expression): BooleanExpression; + +// @beta +export function arrayGet(arrayField: string, offset: number): FunctionExpression; + +// @beta +export function arrayGet(arrayField: string, offsetExpr: Expression): FunctionExpression; + +// @beta +export function arrayGet(arrayExpression: Expression, offset: number): FunctionExpression; + +// @beta +export function arrayGet(arrayExpression: Expression, offsetExpr: Expression): FunctionExpression; + +// @beta +export function arrayLength(fieldName: string): FunctionExpression; + +// @beta +export function arrayLength(array: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function arraySum(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function arraySum(expression: Expression): FunctionExpression; + +// @beta +export function ascending(expr: Expression): Ordering; + +// @beta +export function ascending(fieldName: string): Ordering; + +// @beta +export function average(expression: Expression): AggregateFunction; + +// @beta +export function average(fieldName: string): AggregateFunction; + +// @beta +export class BooleanExpression extends FunctionExpression { + conditional(thenExpr: Expression, elseExpr: Expression): FunctionExpression; + countIf(): AggregateFunction; + // (undocumented) + filterable: true; + ifError(catchValue: BooleanExpression): BooleanExpression; + not(): BooleanExpression; +} + +// @beta +export function byteLength(expr: Expression): FunctionExpression; + +// @beta +export function byteLength(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function ceil(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function ceil(expression: Expression): FunctionExpression; + +// @beta +export function charLength(fieldName: string): FunctionExpression; + +// @beta +export function charLength(stringExpression: Expression): FunctionExpression; + +// @beta (undocumented) +export class CollectionGroupSource extends Stage { + constructor(collectionId: string, options: StageOptions); + } + +// @public +export type CollectionGroupStageOptions = StageOptions & { + collectionId: string; + forceIndex?: string; +}; + +// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function collectionId(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function collectionId(expression: Expression): FunctionExpression; + +// @beta (undocumented) +export class CollectionSource extends Stage { + constructor(collection: string, options: StageOptions); + } + +// @public +export type CollectionStageOptions = StageOptions & { + collection: string | Query; + forceIndex?: string; +}; + +// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function concat(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function concat(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function conditional(condition: BooleanExpression, thenExpr: Expression, elseExpr: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: number): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: string): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// +// @public +export function constant(value: boolean): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: null): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: GeoPoint): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: Timestamp): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function constant(value: Date): Expression; + +// Warning: (ae-forgotten-export) The symbol "Bytes" 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: Bytes): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: DocumentReference): Expression; + +// 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 "Expression" which is marked as @beta +// +// @public +export function constant(value: VectorValue): Expression; + +// @beta +export function cosineDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function cosineDistance(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function cosineDistance(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function cosineDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// @beta +export function count(expression: Expression): 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; + +// Warning: (ae-incompatible-release-tags) The symbol "countDistinct" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "countDistinct" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta +// +// @public +export function countDistinct(expr: Expression | string): AggregateFunction; + +// @beta +export function countIf(booleanExpr: BooleanExpression): AggregateFunction; + +// @beta +export function currentTimestamp(): FunctionExpression; + +// @beta (undocumented) +export class DatabaseSource extends Stage { +} + +// @public +export type DatabaseStageOptions = StageOptions & {}; + +// @beta +export function descending(expr: Expression): Ordering; + +// @beta +export function descending(fieldName: string): Ordering; + +// @beta (undocumented) +export class Distinct extends Stage { + constructor(groups: Map, options: StageOptions); + } + +// @public +export type DistinctStageOptions = StageOptions & { + groups: Array; +}; + +// @beta +export function divide(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function divide(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function divide(fieldName: string, expressions: Expression): FunctionExpression; + +// @beta +export function divide(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function documentId(documentPath: string | DocumentReference): FunctionExpression; + +// @beta +export function documentId(documentPathExpr: Expression): FunctionExpression; + +// @beta (undocumented) +export class DocumentsSource extends Stage { + constructor(docPaths: string[], options: StageOptions); + } + +// @public +export type DocumentsStageOptions = StageOptions & { + docs: Array; +}; + +// @beta +export function dotProduct(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function dotProduct(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function dotProduct(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function dotProduct(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// @beta +export function endsWith(fieldName: string, suffix: string): BooleanExpression; + +// @beta +export function endsWith(fieldName: string, suffix: Expression): BooleanExpression; + +// @beta +export function endsWith(stringExpression: Expression, suffix: string): BooleanExpression; + +// @beta +export function endsWith(stringExpression: Expression, suffix: Expression): BooleanExpression; + +// @beta +export function equal(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function equal(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function equal(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function equal(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function equalAny(expression: Expression, values: Array): BooleanExpression; + +// @beta +export function equalAny(expression: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function equalAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function equalAny(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "error" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function error(message: string): Expression; + +// @beta +export function euclideanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function euclideanDistance(fieldName: string, vectorExpression: Expression): FunctionExpression; + +// @beta +export function euclideanDistance(vectorExpression: Expression, vector: number[] | VectorValue): FunctionExpression; + +// @beta +export function euclideanDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; + +// @public +export function execute(pipeline: Pipeline): Promise; + +// @public (undocumented) +export function execute(options: PipelineExecuteOptions): Promise; + +// @beta +export function exists(value: Expression): BooleanExpression; + +// @beta +export function exists(fieldName: string): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function exp(expression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function exp(fieldName: string): FunctionExpression; + +// @beta +export abstract class Expression { + abs(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + add(second: Expression | unknown): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayConcat(secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayContains(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContains(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAll(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayContainsAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + arrayGet(offset: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayGet(offsetExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arrayReverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + arraySum(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + as(name: string): AliasedExpression; + /* Excluded from this release type: _readUserData */ + ascending(): Ordering; + /* Excluded from this release type: _readUserData */ + average(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + byteLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + ceil(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + charLength(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + collectionId(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + concat(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + cosineDistance(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + cosineDistance(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + count(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + countDistinct(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + descending(): Ordering; + /* Excluded from this release type: _readUserData */ + divide(divisor: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + divide(divisor: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + documentId(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + dotProduct(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + dotProduct(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + endsWith(suffix: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equal(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equal(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equalAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + equalAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vectorExpression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + euclideanDistance(vector: VectorValue | number[]): FunctionExpression; + /* Excluded from this release type: _readUserData */ + exists(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + exp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + // (undocumented) + abstract readonly expressionType: ExpressionType; + /* Excluded from this release type: _readUserData */ + floor(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + greaterThan(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThan(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThanOrEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + greaterThanOrEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + ifAbsent(elseValue: unknown): Expression; + /* Excluded from this release type: _readUserData */ + ifAbsent(elseExpression: unknown): Expression; + /* Excluded from this release type: _readUserData */ + ifError(catchExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + ifError(catchValue: unknown): FunctionExpression; + /* Excluded from this release type: _readUserData */ + isAbsent(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isError(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNan(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNotNan(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNotNull(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + isNull(): BooleanExpression; + /* Excluded from this release type: _readUserData */ + join(delimiterExpression: Expression): Expression; + /* Excluded from this release type: _readUserData */ + join(delimiter: string): Expression; + /* Excluded from this release type: _readUserData */ + length(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + lessThan(experession: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThan(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThanOrEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + lessThanOrEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + like(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + like(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + ln(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + log10(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + logicalMaximum(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + logicalMinimum(second: Expression | unknown, ...others: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapGet(subfield: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapMerge(secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapRemove(key: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mapRemove(keyExpr: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + maximum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + minimum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + mod(expression: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + mod(value: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + multiply(second: Expression | number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + notEqual(expression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqual(value: unknown): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqualAny(values: Array): BooleanExpression; + /* Excluded from this release type: _readUserData */ + notEqualAny(arrayExpression: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + pow(exponent: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + pow(exponent: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexContains(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + regexMatch(pattern: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + reverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(decimalPlaces: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + round(decimalPlaces: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + sqrt(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + startsWith(prefix: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringConcat(secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + /* Excluded from this release type: _readUserData */ + stringContains(substring: string): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringContains(expr: Expression): BooleanExpression; + /* Excluded from this release type: _readUserData */ + stringReverse(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + substring(position: number, length?: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + substring(position: Expression, length?: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + subtract(subtrahend: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + subtract(subtrahend: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + sum(): AggregateFunction; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: Expression, amount: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampAdd(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampSubtract(unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixMicros(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixMillis(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampToUnixSeconds(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + toLower(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + toUpper(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + trim(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixMicrosToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixMillisToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + unixSecondsToTimestamp(): FunctionExpression; + /* Excluded from this release type: _readUserData */ + vectorLength(): FunctionExpression; +} + +// @beta +export type ExpressionType = 'Field' | 'Constant' | 'Function' | 'AggregateFunction' | 'ListOfExpressions' | 'AliasedExpression'; + +// @beta +export class Field extends Expression implements Selectable { + // (undocumented) + get alias(): string; + // (undocumented) + get expr(): Expression; + // (undocumented) + readonly expressionType: ExpressionType; + // (undocumented) + get 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 extends Stage { + constructor(vectorValue: Expression, field: Field, distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', options: StageOptions); + } + +// @public +export type FindNearestStageOptions = StageOptions & { + field: Field | string; + vectorValue: VectorValue | number[]; + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + limit?: number; + distanceField?: string; +}; + +// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function floor(expr: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function floor(fieldName: string): FunctionExpression; + +// @beta +export class FunctionExpression extends Expression { + constructor(name: string, params: Expression[]); + constructor(name: string, params: Expression[], _methodName: string | undefined); + // (undocumented) + readonly expressionType: ExpressionType; + } + +// @beta +export function greaterThan(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function greaterThan(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function greaterThan(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function greaterThan(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function greaterThanOrEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function greaterThanOrEqual(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function greaterThanOrEqual(fieldName: string, value: Expression): BooleanExpression; + +// @beta +export function greaterThanOrEqual(fieldName: string, value: unknown): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function ifAbsent(ifFieldName: string | Expression, elseValue: Expression | unknown): Expression; + +// @beta +export function ifError(tryExpr: BooleanExpression, catchExpr: BooleanExpression): BooleanExpression; + +// @beta +export function ifError(tryExpr: Expression, catchExpr: Expression): FunctionExpression; + +// @beta +export function ifError(tryExpr: Expression, catchValue: unknown): FunctionExpression; + +// @beta +export function isAbsent(value: Expression): BooleanExpression; + +// @beta +export function isAbsent(field: string): BooleanExpression; + +// @beta +export function isError(value: Expression): BooleanExpression; + +// @beta +export function isNan(value: Expression): BooleanExpression; + +// @beta +export function isNan(fieldName: string): BooleanExpression; + +// @beta +export function isNotNan(value: Expression): BooleanExpression; + +// @beta +export function isNotNan(value: string): BooleanExpression; + +// @beta +export function isNotNull(value: Expression): BooleanExpression; + +// @beta +export function isNotNull(value: string): BooleanExpression; + +// @beta +export function isNull(value: Expression): BooleanExpression; + +// @beta +export function isNull(value: string): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function join(arrayFieldName: string, delimiter: string): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function join(arrayExpression: Expression, delimiter: string): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta +// +// @public +export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; + +// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function len(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function len(expression: Expression): FunctionExpression; + +// @beta +export function lessThan(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function lessThan(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function lessThan(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function lessThan(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function lessThanOrEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function lessThanOrEqual(expression: Expression, value: unknown): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// +// @public +export function lessThanOrEqual(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function lessThanOrEqual(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function like(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function like(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function like(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function like(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @beta (undocumented) +export class Limit extends Stage { + constructor(limit: number, options: StageOptions); + } + +// @public +export type LimitStageOptions = StageOptions & { + limit: number; +}; + +// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function ln(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function ln(expression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log(expression: Expression, base: number): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log(expression: Expression, base: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log(fieldName: string, base: number): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log(fieldName: string, base: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log10(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function log10(expression: Expression): FunctionExpression; + +// @beta +export function logicalMaximum(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMaximum(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMinimum(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function logicalMinimum(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function map(elements: Record): FunctionExpression; + +// @beta +export function mapGet(fieldName: string, subField: string): FunctionExpression; + +// @beta +export function mapGet(mapExpression: Expression, subField: string): FunctionExpression; + +// @beta +export function mapMerge(mapField: string, secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + +// @beta +export function mapMerge(firstMap: Record | Expression, secondMap: Record | Expression, ...otherMaps: Array | Expression>): FunctionExpression; + +// @beta +export function mapRemove(mapField: string, key: string): FunctionExpression; + +// @beta +export function mapRemove(mapExpr: Expression, key: string): FunctionExpression; + +// @beta +export function mapRemove(mapField: string, keyExpr: Expression): FunctionExpression; + +// @beta +export function mapRemove(mapExpr: Expression, keyExpr: Expression): FunctionExpression; + +// @beta +export function maximum(expression: Expression): AggregateFunction; + +// @beta +export function maximum(fieldName: string): AggregateFunction; + +// @beta +export function minimum(expression: Expression): AggregateFunction; + +// @beta +export function minimum(fieldName: string): AggregateFunction; + +// @beta +export function mod(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function mod(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function mod(fieldName: string, expression: Expression): FunctionExpression; + +// @beta +export function mod(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function multiply(first: Expression, second: Expression | unknown): FunctionExpression; + +// @beta +export function multiply(fieldName: string, second: Expression | unknown): FunctionExpression; + +// @beta +export function not(booleanExpr: BooleanExpression): BooleanExpression; + +// @beta +export function notEqual(left: Expression, right: Expression): BooleanExpression; + +// @beta +export function notEqual(expression: Expression, value: unknown): BooleanExpression; + +// @beta +export function notEqual(fieldName: string, expression: Expression): BooleanExpression; + +// @beta +export function notEqual(fieldName: string, value: unknown): BooleanExpression; + +// @beta +export function notEqualAny(element: Expression, values: Array): BooleanExpression; + +// @beta +export function notEqualAny(fieldName: string, values: Array): BooleanExpression; + +// @beta +export function notEqualAny(element: Expression, arrayExpression: Expression): BooleanExpression; + +// @beta +export function notEqualAny(fieldName: string, arrayExpression: Expression): BooleanExpression; + +// @beta (undocumented) +export class Offset extends Stage { + constructor(offset: number, options: StageOptions); + } + +// @public +export type OffsetStageOptions = StageOptions & { + offset: number; +}; + +// @public +export type OneOf = { + [K in keyof T]: Pick & { + [P in Exclude]?: undefined; + }; +}[keyof T]; + +// @beta +export function or(first: BooleanExpression, second: BooleanExpression, ...more: BooleanExpression[]): BooleanExpression; + +// @beta +export class Ordering { + constructor(expr: Expression, direction: 'ascending' | 'descending', _methodName: string | undefined); + // (undocumented) + readonly direction: 'ascending' | 'descending'; + // (undocumented) + readonly expr: Expression; +} + +// @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; + // (undocumented) + addFields(options: AddFieldsStageOptions): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "aggregate" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta + aggregate(accumulator: AliasedAggregate, ...additionalAccumulators: AliasedAggregate[]): Pipeline; + aggregate(options: AggregateStageOptions): 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; + // (undocumented) + distinct(options: DistinctStageOptions): Pipeline; + findNearest(options: FindNearestStageOptions): Pipeline; + limit(limit: number): Pipeline; + // (undocumented) + limit(options: LimitStageOptions): Pipeline; + offset(offset: number): Pipeline; + // (undocumented) + offset(options: OffsetStageOptions): Pipeline; + rawStage(name: string, params: unknown[], options?: { [key: string]: Expression | unknown; }): Pipeline; + // 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; + // (undocumented) + removeFields(options: RemoveFieldsStageOptions): Pipeline; + replaceWith(fieldName: string): Pipeline; + // Warning: (ae-incompatible-release-tags) The symbol "replaceWith" is marked as @public, but its signature references "Expression" which is marked as @beta + replaceWith(expr: Expression): Pipeline; + // (undocumented) + replaceWith(options: ReplaceWithStageOptions): Pipeline; + sample(documents: number): Pipeline; + sample(options: SampleStageOptions): 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) + select(options: SelectStageOptions): Pipeline; + // 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) + sort(options: SortStageOptions): Pipeline; + // (undocumented) + stages: any; + union(other: Pipeline): Pipeline; + // (undocumented) + union(options: UnionStageOptions): 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) + unnest(options: UnnestStageOptions): Pipeline; + // (undocumented) + userDataReader: any; + // Warning: (ae-incompatible-release-tags) The symbol "where" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta + where(condition: BooleanExpression): Pipeline; + // (undocumented) + where(options: WhereStageOptions): Pipeline; +} + +// @public +export interface PipelineExecuteOptions { + indexMode?: 'recommended'; + pipeline: Pipeline; + rawOptions?: { + [name: string]: unknown; + }; +} + +// 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; + get(fieldPath: string | FieldPath | Field): any; + get id(): string | undefined; + get ref(): DocumentReference | undefined; + get updateTime(): Timestamp | undefined; +} + +// Warning: (ae-incompatible-release-tags) The symbol "pipelineResultEqual" is marked as @public, but its signature references "PipelineResult" which is marked as @beta +// +// @public (undocumented) +export function pipelineResultEqual(left: PipelineResult, right: PipelineResult): boolean; + +// @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; + // 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(collection: string | Query): PipelineType; + collection(options: CollectionStageOptions): PipelineType; + collectionGroup(collectionId: string): PipelineType; + collectionGroup(options: CollectionGroupStageOptions): PipelineType; + createFrom(query: Query): Pipeline; + database(): PipelineType; + database(options: DatabaseStageOptions): PipelineType; + documents(docs: Array): PipelineType; + documents(options: DocumentsStageOptions): PipelineType; + } + +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function pow(base: Expression, exponent: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function pow(base: Expression, exponent: number): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function pow(base: string, exponent: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function pow(base: string, exponent: number): FunctionExpression; + +// @beta (undocumented) +export class RawStage extends Stage { + } + +// @beta +export function regexContains(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function regexContains(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function regexContains(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function regexContains(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @beta +export function regexMatch(fieldName: string, pattern: string): BooleanExpression; + +// @beta +export function regexMatch(fieldName: string, pattern: Expression): BooleanExpression; + +// @beta +export function regexMatch(stringExpression: Expression, pattern: string): BooleanExpression; + +// @beta +export function regexMatch(stringExpression: Expression, pattern: Expression): BooleanExpression; + +// @public +export type RemoveFieldsStageOptions = StageOptions & { + fields: Array; +}; + +// @public +export type ReplaceWithStageOptions = StageOptions & { + map: Expression | string; +}; + +// @beta +export function reverse(stringExpression: Expression): FunctionExpression; + +// @beta +export function reverse(field: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function round(fieldName: string): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function round(expression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function round(fieldName: string, decimalPlaces: number | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function round(expression: Expression, decimalPlaces: number | Expression): FunctionExpression; + +// @public +export type SampleStageOptions = StageOptions & OneOf<{ + percentage: number; + documents: number; +}>; + +// @beta (undocumented) +export class Select extends Stage { + constructor(selections: Map, options: StageOptions); + } + +// @beta +export interface Selectable { + // (undocumented) + selectable: true; +} + +// @public +export type SelectStageOptions = StageOptions & { + selections: Array; +}; + +// @beta (undocumented) +export class Sort extends Stage { + constructor(orderings: Ordering[], options: StageOptions); + } + +// @public +export type SortStageOptions = StageOptions & { + orderings: Ordering[]; +}; + +// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function sqrt(expression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function sqrt(fieldName: string): FunctionExpression; + +// @beta (undocumented) +export abstract class Stage { + /* Excluded from this release type: optionsProto */ + constructor(options: StageOptions); + // (undocumented) + protected knownOptions: Record; + // (undocumented) + protected rawOptions?: Record; +} + +// @public +export interface StageOptions { + rawOptions?: { + [name: string]: unknown; + }; +} + +// @beta +export function startsWith(fieldName: string, prefix: string): BooleanExpression; + +// @beta +export function startsWith(fieldName: string, prefix: Expression): BooleanExpression; + +// @beta +export function startsWith(stringExpression: Expression, prefix: string): BooleanExpression; + +// @beta +export function startsWith(stringExpression: Expression, prefix: Expression): BooleanExpression; + +// @beta +export function stringConcat(fieldName: string, secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + +// @beta +export function stringConcat(firstString: Expression, secondString: Expression | string, ...otherStrings: Array): FunctionExpression; + +// @beta +export function stringContains(fieldName: string, substring: string): BooleanExpression; + +// @beta +export function stringContains(fieldName: string, substring: Expression): BooleanExpression; + +// @beta +export function stringContains(stringExpression: Expression, substring: string): BooleanExpression; + +// @beta +export function stringContains(stringExpression: Expression, substring: Expression): BooleanExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function stringReverse(stringExpression: Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function stringReverse(field: string): FunctionExpression; + +// @beta +export function substring(field: string, position: number, length?: number): FunctionExpression; + +// @beta +export function substring(input: Expression, position: number, length?: number): FunctionExpression; + +// @beta +export function substring(field: string, position: Expression, length?: Expression): FunctionExpression; + +// @beta +export function substring(input: Expression, position: Expression, length?: Expression): FunctionExpression; + +// @beta +export function subtract(left: Expression, right: Expression): FunctionExpression; + +// @beta +export function subtract(expression: Expression, value: unknown): FunctionExpression; + +// @beta +export function subtract(fieldName: string, expression: Expression): FunctionExpression; + +// @beta +export function subtract(fieldName: string, value: unknown): FunctionExpression; + +// @beta +export function sum(expression: Expression): AggregateFunction; + +// @beta +export function sum(fieldName: string): AggregateFunction; + +// @beta +export function timestampAdd(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; + +// @beta +export function timestampAdd(timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampAdd(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampSubtract(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; + +// @beta +export function timestampSubtract(timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampSubtract(fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number): FunctionExpression; + +// @beta +export function timestampToUnixMicros(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixMicros(fieldName: string): FunctionExpression; + +// @beta +export function timestampToUnixMillis(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixMillis(fieldName: string): FunctionExpression; + +// @beta +export function timestampToUnixSeconds(expr: Expression): FunctionExpression; + +// @beta +export function timestampToUnixSeconds(fieldName: string): FunctionExpression; + +// @beta +export function toLower(fieldName: string): FunctionExpression; + +// @beta +export function toLower(stringExpression: Expression): FunctionExpression; + +// @beta +export function toUpper(fieldName: string): FunctionExpression; + +// @beta +export function toUpper(stringExpression: Expression): FunctionExpression; + +// @beta +export function trim(fieldName: string): FunctionExpression; + +// @beta +export function trim(stringExpression: Expression): FunctionExpression; + +// @public +export type UnionStageOptions = StageOptions & { + other: Pipeline; +}; + +// @beta +export function unixMicrosToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixMicrosToTimestamp(fieldName: string): FunctionExpression; + +// @beta +export function unixMillisToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixMillisToTimestamp(fieldName: string): FunctionExpression; + +// @beta +export function unixSecondsToTimestamp(expr: Expression): FunctionExpression; + +// @beta +export function unixSecondsToTimestamp(fieldName: string): FunctionExpression; + +// @public +export type UnnestStageOptions = StageOptions & { + selectable: Selectable; + indexField?: string; +}; + +// @beta +export function vectorLength(vectorExpression: Expression): FunctionExpression; + +// @beta +export function vectorLength(fieldName: string): FunctionExpression; + +// @beta (undocumented) +export class Where extends Stage { + constructor(condition: BooleanExpression, options: StageOptions); + } + +// @public +export type WhereStageOptions = StageOptions & { + condition: BooleanExpression; +}; + +// @beta +export function xor(first: BooleanExpression, second: BooleanExpression, ...additionalConditions: BooleanExpression[]): BooleanExpression; + + +// Warnings were encountered during analysis: +// +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:73:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:118:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:123:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:785:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:1100:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:3160:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5041:59 - (ae-incompatible-release-tags) The symbol "__index" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5447:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5457:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5584:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5601:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6337:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6386:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta + +// (No @packageDocumentation comment for this package) + +``` diff --git a/package.json b/package.json index 39455ef1161..49fe50d2365 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,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 7d4ab8682ac..9f7dbf9c22f 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..1e5195c8e8c --- /dev/null +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -0,0 +1,173 @@ +/** + * 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 { OneOf } from '../../src/util/types'; + +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 { + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from '../../src/lite-api/stage_options'; + +export { + Expression, + field, + and, + array, + constant, + add, + subtract, + multiply, + average, + substring, + count, + mapMerge, + mapRemove, + ifError, + isAbsent, + isError, + or, + divide, + isNotNan, + map, + isNotNull, + isNull, + mod, + documentId, + equal, + notEqual, + lessThan, + countIf, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, + arrayConcat, + arrayContains, + arrayContainsAny, + arrayContainsAll, + arrayLength, + equalAny, + notEqualAny, + xor, + conditional, + not, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + byteLength, + charLength, + like, + regexContains, + regexMatch, + stringContains, + startsWith, + endsWith, + toLower, + toUpper, + trim, + stringConcat, + mapGet, + countAll, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSubtract, + ascending, + descending, + AliasedExpression, + Field, + Constant, + FunctionExpression, + Ordering, + ExpressionType, + AliasedAggregate, + Selectable, + BooleanExpression, + AggregateFunction +} from '../../src/lite-api/expressions'; diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 4001c19fe8c..e0cde483041 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -8,6 +8,7 @@ "author": "Firebase (https://firebase.google.com/)", "scripts": { "bundle": "rollup -c", + "compile": "tsc --emitDeclarationOnly --declaration -p tsconfig.json", "prebuild": "tsc --emitDeclarationOnly --declaration -p tsconfig.json; yarn api-report", "build": "run-p --npm-path npm build:lite build:main", "build:release": "yarn build && yarn typings:public", @@ -50,11 +51,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,11 +85,37 @@ }, "react-native": "./dist/lite/index.rn.esm.js", "browser": { - "require": "./dist/lite/index.cjs.js", + "require": "./dist/lite/index.browser.cjs.js", "import": "./dist/lite/index.browser.esm.js" }, "default": "./dist/lite/index.browser.esm.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.esm.js", + "browser": { + "require": "./dist/lite/pipelines.browser.cjs.js", + "import": "./dist/lite/pipelines.browser.esm.js" + }, + "default": "./dist/lite/pipelines.browser.esm.js" + }, + "./pipelines": { + "types": "./pipelines/pipelines.d.ts", + "node": { + "require": "./dist/pipelines.node.cjs.js", + "import": "./dist/pipelines.node.mjs" + }, + "react-native": "./dist/index.rn.esm.js", + "browser": { + "require": "./dist/pipelines.cjs.js", + "import": "./dist/pipelines.esm.js" + }, + "default": "./dist/pipelines.esm.js" + }, "./package.json": "./package.json" }, "main": "dist/index.node.cjs.js", @@ -97,7 +126,11 @@ "license": "Apache-2.0", "files": [ "dist", - "lite/package.json" + "lite/package.json", + "pipelines/package.json", + "pipelines/pipelines.d.ts", + "lite/pipelines/package.json", + "lite/pipelines/pipelines.d.ts" ], "dependencies": { "@firebase/component": "0.7.0", @@ -140,7 +173,7 @@ "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, - "typings": "dist/firestore/src/index.d.ts", + "types": "dist/firestore/src/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 5fa616bca80..c9222f69ab4 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 es2020 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 = [ }, // es2020 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].esm.js', + chunkFileNames: 'common-[hash].esm.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 5ea2225f364..6bf9297e4d8 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 es2020 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 es2020 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].esm.js', + chunkFileNames: 'common-[hash].esm.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.esm.js', + chunkFileNames: 'common-[hash].rn.esm.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..843a3696f71 --- /dev/null +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -0,0 +1,162 @@ +/** + * @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 { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; +import { Pipeline as LitePipeline } from '../lite-api/pipeline'; +import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; +import { PipelineSource } from '../lite-api/pipeline-source'; +import { PipelineExecuteOptions } from '../lite-api/pipeline_options'; +import { Stage } from '../lite-api/stage'; +import { + newUserDataReader, + UserDataReader, + UserDataSource +} 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; +export function execute( + options: PipelineExecuteOptions +): Promise; +export function execute( + pipelineOrOptions: LitePipeline | PipelineExecuteOptions +): Promise { + const options: PipelineExecuteOptions = !( + pipelineOrOptions instanceof LitePipeline + ) + ? pipelineOrOptions + : { + pipeline: pipelineOrOptions + }; + + const { pipeline, rawOptions, ...rest } = options; + + const firestore = cast(pipeline._db, Firestore); + const client = ensureFirestoreConfigured(firestore); + + const udr = new UserDataReader( + firestore._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + + const structuredPipelineOptions = new StructuredPipelineOptions( + rest, + rawOptions + ); + structuredPipelineOptions._readUserData(context); + + const structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + return firestoreClientExecutePipeline(client, structuredPipeline).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.fields!, + element.key?.path + ? new DocumentReference(firestore, null, element.key) + : undefined, + 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 { + const userDataReader = newUserDataReader(this); + return new PipelineSource( + this._databaseId, + userDataReader, + (stages: Stage[]) => { + return new Pipeline( + this, + userDataReader, + 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/snapshot.ts b/packages/firestore/src/api/snapshot.ts index c82add0642a..86e075a4ca4 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -31,10 +31,12 @@ import { import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { DocumentSnapshot as LiteDocumentSnapshot, - fieldPathFromArgument, FirestoreDataConverter as LiteFirestoreDataConverter } from '../lite-api/snapshot'; -import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; +import { + fieldPathFromArgument, + UntypedFirestoreDataConverter +} from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { fromBundledQuery } from '../local/local_serializer'; import { documentKeySet } from '../model/collections'; 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..057695ab5f4 --- /dev/null +++ b/packages/firestore/src/api_pipelines.ts @@ -0,0 +1,187 @@ +/** + * @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 { OneOf } from './util/types'; + +export { + PipelineResult, + PipelineSnapshot, + pipelineResultEqual +} from './lite-api/pipeline-result'; + +export { Pipeline } from './api/pipeline'; + +export { execute } from './api/pipeline_impl'; + +export { PipelineExecuteOptions } from './lite-api/pipeline_options'; + +export { + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from './lite-api/stage_options'; + +export { + Stage, + AddFields, + Aggregate, + Distinct, + CollectionSource, + CollectionGroupSource, + DatabaseSource, + DocumentsSource, + Where, + FindNearest, + Limit, + Offset, + Select, + Sort, + RawStage +} from './lite-api/stage'; + +export { + field, + constant, + add, + subtract, + multiply, + divide, + mod, + equal, + notEqual, + lessThan, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, + arrayConcat, + arrayContains, + arrayContainsAny, + arrayContainsAll, + arrayLength, + equalAny, + notEqualAny, + xor, + conditional, + not, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + byteLength, + charLength, + like, + regexContains, + regexMatch, + stringContains, + startsWith, + endsWith, + toLower, + toUpper, + trim, + stringConcat, + mapGet, + countAll, + count, + sum, + average, + and, + or, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSubtract, + ascending, + descending, + countIf, + array, + arrayGet, + isError, + ifError, + isAbsent, + isNull, + isNotNull, + isNotNan, + map, + mapRemove, + mapMerge, + documentId, + substring, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + length as len, + abs, + concat, + currentTimestamp, + error, + ifAbsent, + join, + log10, + arraySum, + Expression, + AliasedExpression, + Field, + FunctionExpression, + Ordering, + BooleanExpression, + AggregateFunction +} from './lite-api/expressions'; + +export type { + ExpressionType, + AliasedAggregate, + Selectable +} 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 39bb8dd4eba..009e7b2aba2 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -38,11 +38,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, @@ -82,6 +87,7 @@ import { removeSnapshotsInSyncListener } from './event_manager'; import { newQueryForPath, Query } from './query'; +import { StructuredPipeline } from './structured_pipeline'; import { SyncEngine } from './sync_engine'; import { syncEngineListen, @@ -550,6 +556,23 @@ export function firestoreClientRunAggregateQuery( return deferred.promise; } +export function firestoreClientExecutePipeline( + client: FirestoreClient, + pipeline: StructuredPipeline +): 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/options_util.ts b/packages/firestore/src/core/options_util.ts new file mode 100644 index 00000000000..f7233850641 --- /dev/null +++ b/packages/firestore/src/core/options_util.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2025 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 { parseData } from '../lite-api/user_data_reader'; +import { ObjectValue } from '../model/object_value'; +import { FieldPath } from '../model/path'; +import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; +import { isPlainObject } from '../util/input_validation'; +import { mapToArray } from '../util/obj'; +export type OptionsDefinitions = Record; +export interface OptionDefinition { + serverName: string; + nestedOptions?: OptionsDefinitions; +} + +export class OptionsUtil { + constructor(private optionDefinitions: OptionsDefinitions) {} + + private _getKnownOptions( + options: Record, + context: ParseContext + ): ObjectValue { + const knownOptions: ObjectValue = ObjectValue.empty(); + + // SERIALIZE KNOWN OPTIONS + for (const knownOptionKey in this.optionDefinitions) { + if (this.optionDefinitions.hasOwnProperty(knownOptionKey)) { + const optionDefinition: OptionDefinition = + this.optionDefinitions[knownOptionKey]; + + if (knownOptionKey in options) { + const optionValue: unknown = options[knownOptionKey]; + let protoValue: Value | undefined = undefined; + + if (optionDefinition.nestedOptions && isPlainObject(optionValue)) { + const nestedUtil = new OptionsUtil(optionDefinition.nestedOptions); + protoValue = { + mapValue: { + fields: nestedUtil.getOptionsProto(context, optionValue) + } + }; + } else if (optionValue) { + protoValue = parseData(optionValue, context) ?? undefined; + } + + if (protoValue) { + knownOptions.set( + FieldPath.fromServerFormat(optionDefinition.serverName), + protoValue + ); + } + } + } + } + + return knownOptions; + } + + getOptionsProto( + context: ParseContext, + knownOptions: Record, + optionsOverride?: Record + ): ApiClientObjectMap | undefined { + const result: ObjectValue = this._getKnownOptions(knownOptions, context); + + // APPLY OPTIONS OVERRIDES + if (optionsOverride) { + const optionsMap = new Map( + mapToArray(optionsOverride, (value, key) => [ + FieldPath.fromServerFormat(key), + value !== undefined ? parseData(value, context) : null + ]) + ); + result.setAll(optionsMap); + } + + // Return MapValue from `result` or empty map value + return result.value.mapValue.fields ?? {}; + } +} diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts new file mode 100644 index 00000000000..3cf754e46f8 --- /dev/null +++ b/packages/firestore/src/core/pipeline-util.ts @@ -0,0 +1,286 @@ +/** + * @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, + BooleanExpression, + and, + or, + Ordering, + lessThan, + greaterThan, + 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): BooleanExpression { + 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.lessThan(Constant._fromProto(value)) + ); + case Operator.LESS_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.lessThanOrEqual(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN: + return and( + fieldValue.exists(), + fieldValue.greaterThan(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.greaterThanOrEqual(Constant._fromProto(value)) + ); + case Operator.EQUAL: + return and( + fieldValue.exists(), + fieldValue.equal(Constant._fromProto(value)) + ); + case Operator.NOT_EQUAL: + return and( + fieldValue.exists(), + fieldValue.notEqual(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.equalAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.equal(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.equalAny(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.notEqualAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.notEqual(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.notEqualAny(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', + undefined + ) + ); +} + +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' +): BooleanExpression { + // The filterFunc is either greater than or less than + const filterFunc = position === 'before' ? lessThan : greaterThan; + 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: BooleanExpression = 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.equal(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.equal(value), condition) + ); + } + + return condition; +} diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts new file mode 100644 index 00000000000..ac8ee4284f6 --- /dev/null +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2025 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 { UserData } from '../lite-api/user_data_reader'; +import { + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, + Pipeline as PipelineProto, + StructuredPipeline as StructuredPipelineProto +} from '../protos/firestore_proto_api'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; + +import { OptionsUtil } from './options_util'; + +export class StructuredPipelineOptions implements UserData { + proto: ApiClientObjectMap | undefined; + + readonly optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode' + } + }); + + constructor( + private _userOptions: Record = {}, + private _optionsOverride: Record = {} + ) {} + + _readUserData(context: ParseContext): void { + this.proto = this.optionsUtil.getOptionsProto( + context, + this._userOptions, + this._optionsOverride + ); + } +} + +export class StructuredPipeline + implements ProtoSerializable +{ + constructor( + private pipeline: ProtoSerializable, + private options: StructuredPipelineOptions + ) {} + + _toProto(serializer: JsonProtoSerializer): StructuredPipelineProto { + return { + pipeline: this.pipeline._toProto(serializer), + options: this.options.proto + }; + } +} 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..c30b2444cf9 --- /dev/null +++ b/packages/firestore/src/lite-api/expressions.ts @@ -0,0 +1,7732 @@ +/** + * @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 +} 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 { vector } from './field_value_impl'; +import { GeoPoint } from './geo_point'; +import { DocumentReference } from './reference'; +import { Timestamp } from './timestamp'; +import { fieldPathFromArgument, parseData, UserData } from './user_data_reader'; +import { VectorValue } from './vector_value'; + +/** + * @beta + * + * An enumeration of the different types of expressions. + */ +export type ExpressionType = + | 'Field' + | 'Constant' + | 'Function' + | 'AggregateFunction' + | 'ListOfExpressions' + | 'AliasedExpression'; + +/** + * 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): Expression { + let result: Expression | undefined; + if (value instanceof Expression) { + return value; + } else if (isPlainObject(value)) { + result = _map(value as Record, undefined); + } else if (value instanceof Array) { + result = array(value); + } else { + result = _constant(value, undefined); + } + + 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[] | Expression): Expression { + if (value instanceof Expression) { + return value; + } else if (value instanceof VectorValue) { + return constant(value); + } else if (Array.isArray(value)) { + return constant(vector(value)); + } else { + throw new Error('Unsupported value: ' + typeof value); + } +} + +/** + * 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): Expression { + if (isString(value)) { + const result = field(value); + 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 Expression implements ProtoValueSerializable, UserData { + abstract readonly expressionType: ExpressionType; + + abstract readonly _methodName?: string; + + /** + * @private + * @internal + */ + abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; + _protoValueType = 'ProtoValue' as const; + + /** + * @private + * @internal + */ + abstract _readUserData(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: Expression | unknown): FunctionExpression { + return new FunctionExpression( + 'add', + [this, valueToDefaultExpr(second)], + 'add' + ); + } + + /** + * 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 subtrahend The expression to subtract from this expression. + * @return A new `Expr` representing the subtraction operation. + */ + subtract(subtrahend: Expression): FunctionExpression; + + /** + * 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 subtrahend The constant value to subtract. + * @return A new `Expr` representing the subtraction operation. + */ + subtract(subtrahend: number): FunctionExpression; + subtract(subtrahend: number | Expression): FunctionExpression { + return new FunctionExpression( + 'subtract', + [this, valueToDefaultExpr(subtrahend)], + 'subtract' + ); + } + + /** + * 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: Expression | number): FunctionExpression { + return new FunctionExpression( + 'multiply', + [this, valueToDefaultExpr(second)], + 'multiply' + ); + } + + /** + * 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 divisor The expression to divide by. + * @return A new `Expr` representing the division operation. + */ + divide(divisor: Expression): FunctionExpression; + + /** + * Creates an expression that divides this expression by a constant value. + * + * ```typescript + * // Divide the 'value' field by 10 + * field("value").divide(10); + * ``` + * + * @param divisor The constant value to divide by. + * @return A new `Expr` representing the division operation. + */ + divide(divisor: number): FunctionExpression; + divide(divisor: number | Expression): FunctionExpression { + return new FunctionExpression( + 'divide', + [this, valueToDefaultExpr(divisor)], + 'divide' + ); + } + + /** + * 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: Expression): FunctionExpression; + + /** + * 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): FunctionExpression; + mod(other: number | Expression): FunctionExpression { + return new FunctionExpression( + 'mod', + [this, valueToDefaultExpr(other)], + 'mod' + ); + } + + /** + * 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").equal(21); + * ``` + * + * @param expression The expression to compare for equality. + * @return A new `Expr` representing the equality comparison. + */ + equal(expression: Expression): BooleanExpression; + + /** + * 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").equal("London"); + * ``` + * + * @param value The constant value to compare for equality. + * @return A new `Expr` representing the equality comparison. + */ + equal(value: unknown): BooleanExpression; + equal(other: unknown): BooleanExpression { + return new BooleanExpression( + 'equal', + [this, valueToDefaultExpr(other)], + 'equal' + ); + } + + /** + * 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").notEqual("completed"); + * ``` + * + * @param expression The expression to compare for inequality. + * @return A new `Expr` representing the inequality comparison. + */ + notEqual(expression: Expression): BooleanExpression; + + /** + * 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").notEqual("USA"); + * ``` + * + * @param value The constant value to compare for inequality. + * @return A new `Expr` representing the inequality comparison. + */ + notEqual(value: unknown): BooleanExpression; + notEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'not_equal', + [this, valueToDefaultExpr(other)], + 'notEqual' + ); + } + + /** + * 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").lessThan(field('limit')); + * ``` + * + * @param experession The expression to compare for less than. + * @return A new `Expr` representing the less than comparison. + */ + lessThan(experession: Expression): BooleanExpression; + + /** + * 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").lessThan(50); + * ``` + * + * @param value The constant value to compare for less than. + * @return A new `Expr` representing the less than comparison. + */ + lessThan(value: unknown): BooleanExpression; + lessThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than', + [this, valueToDefaultExpr(other)], + 'lessThan' + ); + } + + /** + * 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").lessThan(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. + */ + lessThanOrEqual(expression: Expression): BooleanExpression; + + /** + * 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").lessThan(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. + */ + lessThanOrEqual(value: unknown): BooleanExpression; + lessThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than_or_equal', + [this, valueToDefaultExpr(other)], + 'lessThanOrEqual' + ); + } + + /** + * 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").greaterThan(field("limit")); + * ``` + * + * @param expression The expression to compare for greater than. + * @return A new `Expr` representing the greater than comparison. + */ + greaterThan(expression: Expression): BooleanExpression; + + /** + * 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").greaterThan(100); + * ``` + * + * @param value The constant value to compare for greater than. + * @return A new `Expr` representing the greater than comparison. + */ + greaterThan(value: unknown): BooleanExpression; + greaterThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than', + [this, valueToDefaultExpr(other)], + 'greaterThan' + ); + } + + /** + * 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").greaterThanOrEqual(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. + */ + greaterThanOrEqual(expression: Expression): BooleanExpression; + + /** + * 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").greaterThanOrEqual(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. + */ + greaterThanOrEqual(value: unknown): BooleanExpression; + greaterThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than_or_equal', + [this, valueToDefaultExpr(other)], + 'greaterThanOrEqual' + ); + } + + /** + * 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: Expression | unknown[], + ...otherArrays: Array + ): FunctionExpression { + const elements = [secondArray, ...otherArrays]; + const exprValues = elements.map(value => valueToDefaultExpr(value)); + return new FunctionExpression( + 'array_concat', + [this, ...exprValues], + 'arrayConcat' + ); + } + + /** + * 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: Expression): BooleanExpression; + + /** + * 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): BooleanExpression; + arrayContains(element: unknown): BooleanExpression { + return new BooleanExpression( + 'array_contains', + [this, valueToDefaultExpr(element)], + 'arrayContains' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + arrayContainsAll(values: unknown[] | Expression): BooleanExpression { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAll') + : values; + return new BooleanExpression( + 'array_contains_all', + [this, normalizedExpr], + 'arrayContainsAll' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + arrayContainsAny( + values: Array | Expression + ): BooleanExpression { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAny') + : values; + return new BooleanExpression( + 'array_contains_any', + [this, normalizedExpr], + 'arrayContainsAny' + ); + } + + /** + * Creates an expression that reverses an array. + * + * ```typescript + * // Reverse the value of the 'myArray' field. + * field("myArray").arrayReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed array. + */ + arrayReverse(): FunctionExpression { + return new FunctionExpression('array_reverse', [this]); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('array_length', [this], 'arrayLength'); + } + + /** + * 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").equalAny("Electronics", field("primaryType")); + * ``` + * + * @param values The values or expressions to check against. + * @return A new `Expr` representing the 'IN' comparison. + */ + equalAny(values: Array): BooleanExpression; + + /** + * 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").equalAny(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. + */ + equalAny(arrayExpression: Expression): BooleanExpression; + equalAny(others: unknown[] | Expression): BooleanExpression { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'equalAny') + : others; + return new BooleanExpression('equal_any', [this, exprOthers], 'equalAny'); + } + + /** + * 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").notEqualAny(["pending", field("rejectedStatus")]); + * ``` + * + * @param values The values or expressions to check against. + * @return A new `Expr` representing the 'notEqualAny' comparison. + */ + notEqualAny(values: Array): BooleanExpression; + + /** + * 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").notEqualAny(field('rejectedStatuses')); + * ``` + * + * @param arrayExpression The values or expressions to check against. + * @return A new `Expr` representing the 'notEqualAny' comparison. + */ + notEqualAny(arrayExpression: Expression): BooleanExpression; + notEqualAny(others: unknown[] | Expression): BooleanExpression { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'notEqualAny') + : others; + return new BooleanExpression( + 'not_equal_any', + [this, exprOthers], + 'notEqualAny' + ); + } + + /** + * 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(): BooleanExpression { + return new BooleanExpression('is_nan', [this], 'isNan'); + } + + /** + * 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(): BooleanExpression { + return new BooleanExpression('is_null', [this], 'isNull'); + } + + /** + * 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(): BooleanExpression { + return new BooleanExpression('exists', [this], 'exists'); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('char_length', [this], 'charLength'); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + like(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'like', + [this, valueToDefaultExpr(stringOrExpr)], + 'like' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + regexContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexContains' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + regexMatch(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_match', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexMatch' + ); + } + + /** + * Creates an expression that checks if a string contains a specified substring. + * + * ```typescript + * // Check if the 'description' field contains "example". + * field("description").stringContains("example"); + * ``` + * + * @param substring The substring to search for. + * @return A new `Expr` representing the 'contains' comparison. + */ + stringContains(substring: string): BooleanExpression; + + /** + * 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").stringContains(field("keyword")); + * ``` + * + * @param expr The expression representing the substring to search for. + * @return A new `Expr` representing the 'contains' comparison. + */ + stringContains(expr: Expression): BooleanExpression; + stringContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'string_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'stringContains' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + startsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'starts_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'startsWith' + ); + } + + /** + * 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): BooleanExpression; + + /** + * 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: Expression): BooleanExpression; + endsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'ends_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'endsWith' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('to_lower', [this], 'toLower'); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('to_upper', [this], 'toUpper'); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('trim', [this], 'trim'); + } + + /** + * Creates an expression that concatenates string expressions together. + * + * ```typescript + * // Combine the 'firstName', " ", and 'lastName' fields into a single string + * field("firstName").stringConcat(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. + */ + stringConcat( + secondString: Expression | string, + ...otherStrings: Array + ): FunctionExpression { + const elements = [secondString, ...otherStrings]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpression( + 'string_concat', + [this, ...exprs], + 'stringConcat' + ); + } + + /** + * Creates an expression that concatenates expression results together. + * + * ```typescript + * // Combine the 'firstName', ' ', and 'lastName' fields into a single value. + * field("firstName").concat(constant(" "), field("lastName")); + * ``` + * + * @param second The additional expression or literal to concatenate. + * @param others Optional additional expressions or literals to concatenate. + * @return A new `Expr` representing the concatenated value. + */ + concat( + second: Expression | unknown, + ...others: Array + ): FunctionExpression { + const elements = [second, ...others]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpression('concat', [this, ...exprs], 'concat'); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('reverse', [this], 'reverse'); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('byte_length', [this], 'byteLength'); + } + + /** + * Creates an expression that computes the ceiling of a numeric value. + * + * ```typescript + * // Compute the ceiling of the 'price' field. + * field("price").ceil(); + * ``` + * + * @return A new {@code Expr} representing the ceiling of the numeric value. + */ + ceil(): FunctionExpression { + return new FunctionExpression('ceil', [this]); + } + + /** + * Creates an expression that computes the floor of a numeric value. + * + * ```typescript + * // Compute the floor of the 'price' field. + * field("price").floor(); + * ``` + * + * @return A new {@code Expr} representing the floor of the numeric value. + */ + floor(): FunctionExpression { + return new FunctionExpression('floor', [this]); + } + + /** + * Creates an expression that computes the absolute value of a numeric value. + * + * ```typescript + * // Compute the absolute value of the 'price' field. + * field("price").abs(); + * ``` + * + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ + abs(): FunctionExpression { + return new FunctionExpression('abs', [this]); + } + + /** + * Creates an expression that computes e to the power of this expression. + * + * ```typescript + * // Compute e to the power of the 'value' field. + * field("value").exp(); + * ``` + * + * @return A new {@code Expr} representing the exp of the numeric value. + */ + exp(): FunctionExpression { + return new FunctionExpression('exp', [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): FunctionExpression { + return new FunctionExpression( + 'map_get', + [this, constant(subfield)], + 'mapGet' + ); + } + + /** + * 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], 'count'); + } + + /** + * 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], 'sum'); + } + + /** + * 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").average().as("averageAge"); + * ``` + * + * @return A new `AggregateFunction` representing the 'average' aggregation. + */ + average(): AggregateFunction { + return new AggregateFunction('average', [this], 'average'); + } + + /** + * 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 'minimum' aggregation. + */ + minimum(): AggregateFunction { + return new AggregateFunction('minimum', [this], 'minimum'); + } + + /** + * 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 'maximum' aggregation. + */ + maximum(): AggregateFunction { + return new AggregateFunction('maximum', [this], 'maximum'); + } + + /** + * Creates an aggregation that counts the number of distinct values of the expression or field. + * + * ```typescript + * // Count the distinct number of products + * field("productId").countDistinct().as("distinctProducts"); + * ``` + * + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ + countDistinct(): AggregateFunction { + return new AggregateFunction('count_distinct', [this], 'countDistinct'); + } + + /** + * 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 maximum operation. + */ + logicalMaximum( + second: Expression | unknown, + ...others: Array + ): FunctionExpression { + const values = [second, ...others]; + return new FunctionExpression( + 'maximum', + [this, ...values.map(valueToDefaultExpr)], + 'logicalMaximum' + ); + } + + /** + * 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 minimum operation. + */ + logicalMinimum( + second: Expression | unknown, + ...others: Array + ): FunctionExpression { + const values = [second, ...others]; + return new FunctionExpression( + 'minimum', + [this, ...values.map(valueToDefaultExpr)], + 'minimum' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression('vector_length', [this], 'vectorLength'); + } + + /** + * 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: Expression): FunctionExpression; + /** + * 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[]): FunctionExpression; + cosineDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'cosine_distance', + [this, vectorToExpr(other)], + 'cosineDistance' + ); + } + + /** + * 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: Expression): FunctionExpression; + + /** + * 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[]): FunctionExpression; + dotProduct(other: Expression | VectorValue | number[]): FunctionExpression { + return new FunctionExpression( + 'dot_product', + [this, vectorToExpr(other)], + 'dotProduct' + ); + } + + /** + * 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: Expression): FunctionExpression; + + /** + * 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[]): FunctionExpression; + euclideanDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'euclidean_distance', + [this, vectorToExpr(other)], + 'euclideanDistance' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'unix_micros_to_timestamp', + [this], + 'unixMicrosToTimestamp' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_micros', + [this], + 'timestampToUnixMicros' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'unix_millis_to_timestamp', + [this], + 'unixMillisToTimestamp' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_millis', + [this], + 'timestampToUnixMillis' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'unix_seconds_to_timestamp', + [this], + 'unixSecondsToTimestamp' + ); + } + + /** + * 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(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_seconds', + [this], + 'timestampToUnixSeconds' + ); + } + + /** + * 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: Expression, amount: Expression): FunctionExpression; + + /** + * 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 + ): FunctionExpression; + timestampAdd( + unit: + | Expression + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_add', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampAdd' + ); + } + + /** + * 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").timestampSubtract(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. + */ + timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; + + /** + * Creates an expression that subtracts a specified amount of time from this timestamp expression. + * + * ```typescript + * // Subtract 1 day from the 'timestamp' field. + * field("timestamp").timestampSubtract("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. + */ + timestampSubtract( + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number + ): FunctionExpression; + timestampSubtract( + unit: + | Expression + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_subtract', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampSubtract' + ); + } + + /** + * @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(): FunctionExpression { + return new FunctionExpression('document_id', [this], 'documentId'); + } + + /** + * @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. + */ + substring(position: number, length?: number): FunctionExpression; + + /** + * @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. + */ + substring(position: Expression, length?: Expression): FunctionExpression; + substring( + position: Expression | number, + length?: Expression | number + ): FunctionExpression { + const positionExpr = valueToDefaultExpr(position); + if (length === undefined) { + return new FunctionExpression( + 'substring', + [this, positionExpr], + 'substring' + ); + } else { + return new FunctionExpression( + 'substring', + [this, positionExpr, valueToDefaultExpr(length)], + 'substring' + ); + } + } + + /** + * @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').arrayGet(1); + * ``` + * + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayGet' operation. + */ + arrayGet(offset: number): FunctionExpression; + + /** + * @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').arrayGet(field('favoriteTag')); + * ``` + * + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayGet' operation. + */ + arrayGet(offsetExpr: Expression): FunctionExpression; + arrayGet(offset: Expression | number): FunctionExpression { + return new FunctionExpression( + 'array_get', + [this, valueToDefaultExpr(offset)], + 'arrayGet' + ); + } + + /** + * @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(): BooleanExpression { + return new BooleanExpression('is_error', [this], 'isError'); + } + + /** + * @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").arrayGet(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: Expression): FunctionExpression; + + /** + * @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").arrayGet(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): FunctionExpression; + ifError(catchValue: unknown): FunctionExpression { + return new FunctionExpression( + 'if_error', + [this, valueToDefaultExpr(catchValue)], + 'ifError' + ); + } + + /** + * @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(): BooleanExpression { + return new BooleanExpression('is_absent', [this], 'isAbsent'); + } + + /** + * @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(): BooleanExpression { + return new BooleanExpression('is_not_null', [this], '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 + * field("value").divide(0).isNotNan(); + * ``` + * + * @return A new {@code Expr} representing the 'isNaN' check. + */ + isNotNan(): BooleanExpression { + return new BooleanExpression('is_not_nan', [this], 'isNotNan'); + } + + /** + * @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): FunctionExpression; + /** + * @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: Expression): FunctionExpression; + mapRemove(stringExpr: Expression | string): FunctionExpression { + return new FunctionExpression( + 'map_remove', + [this, valueToDefaultExpr(stringExpr)], + 'mapRemove' + ); + } + + /** + * @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 }, conditional(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 | Expression, + ...otherMaps: Array | Expression> + ): FunctionExpression { + const secondMapExpr = valueToDefaultExpr(secondMap); + const otherMapExprs = otherMaps.map(valueToDefaultExpr); + return new FunctionExpression( + 'map_merge', + [this, secondMapExpr, ...otherMapExprs], + 'mapMerge' + ); + } + + /** + * Creates an expression that returns the value of this expression raised to the power of another expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * field("base").pow(field("exponent")); + * ``` + * + * @param exponent The expression to raise this expression to the power of. + * @return A new `Expr` representing the power operation. + */ + pow(exponent: Expression): FunctionExpression; + + /** + * Creates an expression that returns the value of this expression raised to the power of a constant value. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * field("base").pow(2); + * ``` + * + * @param exponent The constant value to raise this expression to the power of. + * @return A new `Expr` representing the power operation. + */ + pow(exponent: number): FunctionExpression; + pow(exponent: number | Expression): FunctionExpression { + return new FunctionExpression('pow', [this, valueToDefaultExpr(exponent)]); + } + + /** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * field("price").round(); + * ``` + * + * @return A new `Expr` representing the rounded value. + */ + round(): FunctionExpression; + /** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * field("price").round(2); + * ``` + * + * @param decimalPlaces A constant specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. + */ + round(decimalPlaces: number): FunctionExpression; + /** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * field("price").round(constant(2)); + * ``` + * + * @param decimalPlaces An expression specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. + */ + round(decimalPlaces: Expression): FunctionExpression; + round(decimalPlaces?: number | Expression): FunctionExpression { + if (decimalPlaces === undefined) { + return new FunctionExpression('round', [this]); + } else { + return new FunctionExpression( + 'round', + [this, valueToDefaultExpr(decimalPlaces)], + 'round' + ); + } + } + + /** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * field("__path__").collectionId(); + * ``` + * + * @return A new {@code Expr} representing the collectionId operation. + */ + collectionId(): FunctionExpression { + return new FunctionExpression('collection_id', [this]); + } + + /** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * field("name").length(); + * + * // Get the number of items in the 'cart' array. + * field("cart").length(); + * ``` + * + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ + length(): FunctionExpression { + return new FunctionExpression('length', [this]); + } + + /** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * field("value").ln(); + * ``` + * + * @return A new {@code Expr} representing the natural logarithm of the numeric value. + */ + ln(): FunctionExpression { + return new FunctionExpression('ln', [this]); + } + + /** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * field("value").sqrt(); + * ``` + * + * @return A new {@code Expr} representing the square root of the numeric value. + */ + sqrt(): FunctionExpression { + return new FunctionExpression('sqrt', [this]); + } + + /** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * field("myString").stringReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed string. + */ + stringReverse(): FunctionExpression { + return new FunctionExpression('string_reverse', [this]); + } + + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of the this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * field("optional_field").ifAbsent("default_value") + * ``` + * + * @param elseValue The value that will be returned if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseValue: unknown): Expression; + + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or if that is + * // absent, then returns the value of the field ` + * field("optional_field").ifAbsent(field('default_field')) + * ``` + * + * @param elseExpression The Expression that will be evaluated if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseExpression: unknown): Expression; + + ifAbsent(elseValueOrExpression: Expression | unknown): Expression { + return new FunctionExpression( + 'if_absent', + [this, valueToDefaultExpr(elseValueOrExpression)], + 'ifAbsent' + ); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * field("tags").join(field("separator")) + * ``` + * + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ + join(delimiterExpression: Expression): Expression; + + /** + * Creates an expression that joins the elements of an array field into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * field("tags").join(", ") + * ``` + * + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ + join(delimiter: string): Expression; + + join(delimeterValueOrExpression: string | Expression): Expression { + return new FunctionExpression( + 'join', + [this, valueToDefaultExpr(delimeterValueOrExpression)], + 'join' + ); + } + + /** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * field("value").log10(); + * ``` + * + * @return A new {@code Expr} representing the base-10 logarithm of the numeric value. + */ + log10(): FunctionExpression { + return new FunctionExpression('log10', [this]); + } + + /** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * field("scores").arraySum(); + * ``` + * + * @return A new {@code Expr} representing the sum of the elements in the array. + */ + arraySum(): FunctionExpression { + return new FunctionExpression('sum', [this]); + } + + // TODO(new-expression): Add new expression method definitions above this line + + /** + * 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 AliasedExpression} that wraps this + * expression and associates it with the provided alias. + */ + as(name: string): AliasedExpression { + return new AliasedExpression(this, name, 'as'); + } +} + +/** + * @beta + * + * An interface that represents a selectable expression. + */ +export interface Selectable { + selectable: true; + /** + * @private + * @internal + */ + readonly alias: string; + /** + * @private + * @internal + */ + readonly expr: Expression; +} + +/** + * @beta + * + * A class that represents an aggregate function. + */ +export class AggregateFunction implements ProtoValueSerializable, UserData { + exprType: ExpressionType = 'AggregateFunction'; + + constructor(name: string, params: Expression[]); + /** + * INTERNAL Constructor with method name for validation. + * @hideconstructor + * @param name + * @param params + * @param _methodName + */ + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) {} + + /** + * 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").average().as("averagePrice")); + * ``` + * + * @param name The alias to assign to this AggregateFunction. + * @return A new {@link AliasedAggregate} that wraps this + * AggregateFunction and associates it with the provided alias. + */ + as(name: string): AliasedAggregate { + return new AliasedAggregate(this, name, 'as'); + } + + /** + * @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(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; + this.params.forEach(expr => { + return expr._readUserData(context); + }); + } +} + +/** + * @beta + * + * An AggregateFunction with alias. + */ +export class AliasedAggregate implements UserData { + constructor( + readonly aggregate: AggregateFunction, + readonly alias: string, + readonly _methodName: string | undefined + ) {} + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + this.aggregate._readUserData(context); + } +} + +/** + * @beta + */ +export class AliasedExpression implements Selectable, UserData { + exprType: ExpressionType = 'AliasedExpression'; + selectable = true as const; + + constructor( + readonly expr: Expression, + readonly alias: string, + readonly _methodName: string | undefined + ) {} + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); + } +} + +/** + * @internal + */ +class ListOfExprs extends Expression implements UserData { + expressionType: ExpressionType = 'ListOfExpressions'; + + constructor( + private exprs: Expression[], + readonly _methodName: string | undefined + ) { + super(); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + arrayValue: { + values: this.exprs.map(p => p._toProto(serializer)!) + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + this.exprs.forEach((expr: Expression) => expr._readUserData(context)); + } +} + +/** + * @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 Expression implements Selectable { + readonly expressionType: ExpressionType = 'Field'; + selectable = true as const; + + /** + * @internal + * @private + * @hideconstructor + * @param fieldPath + */ + constructor( + private fieldPath: InternalFieldPath, + readonly _methodName: string | undefined + ) { + super(); + } + + get fieldName(): string { + return this.fieldPath.canonicalString(); + } + + get alias(): string { + return this.fieldName; + } + + get expr(): Expression { + return this; + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + fieldReferenceValue: this.fieldPath.canonicalString() + }; + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): 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 { + return _field(nameOrPath, 'field'); +} + +export function _field( + nameOrPath: string | FieldPath, + methodName: string | undefined +): Field { + if (typeof nameOrPath === 'string') { + if (DOCUMENT_KEY_NAME === nameOrPath) { + return new Field(documentIdFieldPath()._internalPath, methodName); + } + return new Field(fieldPathFromArgument('field', nameOrPath), methodName); + } else { + return new Field(nameOrPath._internalPath, methodName); + } +} + +/** + * @internal + * + * 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 Expression { + readonly expressionType: ExpressionType = 'Constant'; + + private _protoValue?: ProtoValue; + + /** + * @private + * @internal + * @hideconstructor + * @param value The value of the constant. + */ + constructor( + private value: unknown, + readonly _methodName: string | undefined + ) { + super(); + } + + /** + * @private + * @internal + */ + static _fromProto(value: ProtoValue): Constant { + const result = new Constant(value, undefined); + result._protoValue = value; + return result; + } + + /** + * @private + * @internal + */ + _toProto(_: JsonProtoSerializer): ProtoValue { + hardAssert( + this._protoValue !== undefined, + 0x00ed, + 'Value of this constant has not been serialized to proto value' + ); + return this._protoValue; + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; + 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): Expression; + +/** + * Creates a `Constant` instance for a string value. + * + * @param value The string value. + * @return A new `Constant` instance. + */ +export function constant(value: string): Expression; + +/** + * Creates a `BooleanExpression` instance for a boolean value. + * + * @param value The boolean value. + * @return A new `Constant` instance. + */ +export function constant(value: boolean): BooleanExpression; + +/** + * Creates a `Constant` instance for a null value. + * + * @param value The null value. + * @return A new `Constant` instance. + */ +export function constant(value: null): Expression; + +/** + * Creates a `Constant` instance for a GeoPoint value. + * + * @param value The GeoPoint value. + * @return A new `Constant` instance. + */ +export function constant(value: GeoPoint): Expression; + +/** + * Creates a `Constant` instance for a Timestamp value. + * + * @param value The Timestamp value. + * @return A new `Constant` instance. + */ +export function constant(value: Timestamp): Expression; + +/** + * Creates a `Constant` instance for a Date value. + * + * @param value The Date value. + * @return A new `Constant` instance. + */ +export function constant(value: Date): Expression; + +/** + * Creates a `Constant` instance for a Bytes value. + * + * @param value The Bytes value. + * @return A new `Constant` instance. + */ +export function constant(value: Bytes): Expression; + +/** + * Creates a `Constant` instance for a DocumentReference value. + * + * @param value The DocumentReference value. + * @return A new `Constant` instance. + */ +export function constant(value: DocumentReference): Expression; + +/** + * 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): Expression; + +/** + * Creates a `Constant` instance for a VectorValue value. + * + * @param value The VectorValue value. + * @return A new `Constant` instance. + */ +export function constant(value: VectorValue): Expression; + +export function constant(value: unknown): Expression | BooleanExpression { + return _constant(value, 'constant'); +} + +/** + * @internal + * @private + * @param value + * @param methodName + */ +export function _constant( + value: unknown, + methodName: string | undefined +): Constant | BooleanExpression { + if (typeof value === 'boolean') { + return new BooleanConstant(value, methodName); + } else { + return new Constant(value, methodName); + } +} + +/** + * Internal only + * @internal + * @private + */ +export class MapValue extends Expression { + constructor( + private plainObject: Map, + readonly _methodName: string | undefined + ) { + super(); + } + + expressionType: ExpressionType = 'Constant'; + + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; + this.plainObject.forEach(expr => { + expr._readUserData(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 equal}, + * or the methods on {@link Expression} ({@link Expression#equal}, {@link Expression#lessThan}, etc.) to construct new Function instances. + */ +export class FunctionExpression extends Expression { + readonly expressionType: ExpressionType = 'Function'; + + constructor(name: string, params: Expression[]); + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) { + super(); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + functionValue: { + name: this.name, + args: this.params.map(p => p._toProto(serializer)) + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; + this.params.forEach(expr => { + return expr._readUserData(context); + }); + } +} + +/** + * @beta + * + * An interface that represents a filter condition. + */ +export class BooleanExpression extends FunctionExpression { + 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").greaterThan(90).countIf().as("highestScore"); + * ``` + * + * @return A new `AggregateFunction` representing the 'countIf' aggregation. + */ + countIf(): AggregateFunction { + return new AggregateFunction('count_if', [this], 'countIf'); + } + + /** + * 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(): BooleanExpression { + return new BooleanExpression('not', [this], 'not'); + } + + /** + * Creates a conditional expression that evaluates to the 'then' expression + * if `this` expression evaluates to `true`, + * or evaluates to the 'else' expression if `this` expressions evaluates `false`. + * + * ```typescript + * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". + * field("age").greaterThanOrEqual(18).conditional(constant("Adult"), constant("Minor")); + * ``` + * + * @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. + */ + conditional(thenExpr: Expression, elseExpr: Expression): FunctionExpression { + return new FunctionExpression( + 'conditional', + [this, thenExpr, elseExpr], + 'conditional' + ); + } + + /** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of this expression. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * constant(50).divide('length').gt(1).ifError(constant(false)); + * ``` + * + * @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: BooleanExpression): BooleanExpression { + return new BooleanExpression('if_error', [this, catchValue], 'ifError'); + } +} + +/** + * @private + * @internal + * + * To return a BooleanExpr as a constant, we need to break the pattern that expects a BooleanExpr to be a + * "pipeline function". Instead of building on serialization logic built into BooleanExpr, + * we override methods with those of an internally kept Constant value. + */ +export class BooleanConstant extends BooleanExpression { + private readonly _internalConstant: Constant; + + constructor(value: boolean, readonly _methodName?: string) { + super('', []); + + this._internalConstant = new Constant(value, _methodName); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return this._internalConstant._toProto(serializer); + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + return this._internalConstant._readUserData(context); + } +} + +/** + * @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").equal(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: BooleanExpression): AggregateFunction { + return booleanExpr.countIf(); +} + +/** + * @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. + * arrayGet('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 'arrayGet' operation. + */ +export function arrayGet( + arrayField: string, + offset: number +): FunctionExpression; + +/** + * @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'. + * arrayGet('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 'arrayGet' operation. + */ +export function arrayGet( + arrayField: string, + offsetExpr: Expression +): FunctionExpression; + +/** + * @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. + * arrayGet(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 'arrayGet' operation. + */ +export function arrayGet( + arrayExpression: Expression, + offset: number +): FunctionExpression; + +/** + * @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'. + * arrayGet(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 'arrayGet' operation. + */ +export function arrayGet( + arrayExpression: Expression, + offsetExpr: Expression +): FunctionExpression; +export function arrayGet( + array: Expression | string, + offset: Expression | number +): FunctionExpression { + return fieldOrExpression(array).arrayGet(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: Expression): BooleanExpression { + 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. + * + * This overload is useful when a BooleanExpression is required. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * ifError(constant(50).divide('length').gt(1), constant(false)); + * ``` + * + * @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: BooleanExpression, + catchExpr: BooleanExpression +): BooleanExpression; + +/** + * @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").arrayGet(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: Expression, + catchExpr: Expression +): FunctionExpression; + +/** + * @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").arrayGet(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: Expression, + catchValue: unknown +): FunctionExpression; +export function ifError( + tryExpr: Expression, + catchValue: unknown +): FunctionExpression { + if ( + tryExpr instanceof BooleanExpression && + catchValue instanceof BooleanExpression + ) { + return tryExpr.ifError(catchValue); + } else { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function isAbsent(value: Expression | string): BooleanExpression { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function isNull(value: Expression | string): BooleanExpression { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function isNotNull(value: Expression | string): BooleanExpression { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function isNotNan(value: Expression | string): BooleanExpression { + 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): FunctionExpression; +/** + * @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: Expression, key: string): FunctionExpression; +/** + * @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: Expression +): FunctionExpression; +/** + * @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: Expression, + keyExpr: Expression +): FunctionExpression; + +export function mapRemove( + mapExpr: Expression | string, + stringExpr: Expression | string +): FunctionExpression { + 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 }, conditional(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 | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; + +/** + * @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 }, conditional(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 | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; + +export function mapMerge( + firstMap: string | Record | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression { + 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 +): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; + +export function documentId( + documentPath: Expression | string | DocumentReference +): FunctionExpression { + // @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 substring( + field: string, + position: number, + length?: number +): FunctionExpression; + +/** + * @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 substring( + input: Expression, + position: number, + length?: number +): FunctionExpression; + +/** + * @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 substring( + field: string, + position: Expression, + length?: Expression +): FunctionExpression; + +/** + * @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 substring( + input: Expression, + position: Expression, + length?: Expression +): FunctionExpression; + +export function substring( + field: Expression | string, + position: Expression | number, + length?: Expression | number +): FunctionExpression { + const fieldExpr = fieldOrExpression(field); + const positionExpr = valueToDefaultExpr(position); + const lengthExpr = + length === undefined ? undefined : valueToDefaultExpr(length); + return fieldExpr.substring(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: Expression, + second: Expression | unknown +): FunctionExpression; + +/** + * @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: Expression | unknown +): FunctionExpression; + +export function add( + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).add(valueToDefaultExpr(second)); +} + +/** + * @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: Expression, + right: Expression +): FunctionExpression; + +/** + * @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: Expression, + value: unknown +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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): FunctionExpression; +export function subtract( + left: Expression | string, + right: Expression | unknown +): FunctionExpression { + 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: Expression, + second: Expression | unknown +): FunctionExpression; + +/** + * @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: Expression | unknown +): FunctionExpression; + +export function multiply( + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).multiply(valueToDefaultExpr(second)); +} + +/** + * @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: Expression, right: Expression): FunctionExpression; + +/** + * @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: Expression, + value: unknown +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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): FunctionExpression; +export function divide( + left: Expression | string, + right: Expression | unknown +): FunctionExpression { + 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: Expression, right: Expression): FunctionExpression; + +/** + * @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: Expression, value: unknown): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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): FunctionExpression; +export function mod( + left: Expression | string, + right: Expression | unknown +): FunctionExpression { + 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): FunctionExpression { + return _map(elements, 'map'); +} +export function _map( + elements: Record, + methodName: string | undefined +): FunctionExpression { + const result: Expression[] = []; + 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 FunctionExpression('map', result, 'map'); +} + +/** + * 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, undefined); +} + +/** + * @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[]): FunctionExpression { + return _array(elements, 'array'); +} +export function _array( + elements: unknown[], + methodName: string | undefined +): FunctionExpression { + return new FunctionExpression( + 'array', + elements.map(element => valueToDefaultExpr(element)), + methodName + ); +} + +/** + * @beta + * + * Creates an expression that checks if two expressions are equal. + * + * ```typescript + * // Check if the 'age' field is equal to an expression + * equal(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 equal(left: Expression, right: Expression): BooleanExpression; + +/** + * @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 + * equal(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 equal( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * @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 + * equal("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 equal( + fieldName: string, + expression: Expression +): BooleanExpression; + +/** + * @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" + * equal("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 equal(fieldName: string, value: unknown): BooleanExpression; +export function equal( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.equal(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' + * notEqual(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 notEqual( + left: Expression, + right: Expression +): BooleanExpression; + +/** + * @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" + * notEqual(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 notEqual( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * @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' + * notEqual("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 notEqual( + fieldName: string, + expression: Expression +): BooleanExpression; + +/** + * @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" + * notEqual("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 notEqual(fieldName: string, value: unknown): BooleanExpression; +export function notEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.notEqual(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 + * lessThan(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 lessThan( + left: Expression, + right: Expression +): BooleanExpression; + +/** + * @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 + * lessThan(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 lessThan( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * @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 + * lessThan("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 lessThan( + fieldName: string, + expression: Expression +): BooleanExpression; + +/** + * @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 + * lessThan("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 lessThan(fieldName: string, value: unknown): BooleanExpression; +export function lessThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lessThan(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 + * lessThan(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 lessThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; + +/** + * @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 + * lessThan(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 lessThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * 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 + * lessThan("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 lessThanOrEqual( + fieldName: string, + expression: Expression +): BooleanExpression; + +/** + * @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 + * lessThan("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 lessThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function lessThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lessThanOrEqual(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 + * greaterThan(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 greaterThan( + left: Expression, + right: Expression +): BooleanExpression; + +/** + * @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 + * greaterThan(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 greaterThan( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * @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' + * greaterThan("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 greaterThan( + fieldName: string, + expression: Expression +): BooleanExpression; + +/** + * @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 + * greaterThan("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 greaterThan( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.greaterThan(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" + * greaterThanOrEqual(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 greaterThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; + +/** + * @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 + * greaterThanOrEqual(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 greaterThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; + +/** + * @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' + * greaterThanOrEqual("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 greaterThanOrEqual( + fieldName: string, + value: Expression +): BooleanExpression; + +/** + * @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 + * greaterThanOrEqual("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 greaterThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.greaterThanOrEqual(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: Expression, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; + +/** + * @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: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; + +export function arrayConcat( + firstArray: Expression | string, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression { + 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: Expression, + element: Expression +): FunctionExpression; + +/** + * @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: Expression, + element: unknown +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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 +): BooleanExpression; +export function arrayContains( + array: Expression | string, + element: unknown +): BooleanExpression { + 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: Expression, + values: Array +): BooleanExpression; + +/** + * @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 +): BooleanExpression; + +/** + * @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: Expression, + values: Expression +): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; +export function arrayContainsAny( + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { + // @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: Expression, + values: Array +): BooleanExpression; + +/** + * @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 +): BooleanExpression; + +/** + * @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: Expression, + arrayExpression: Expression +): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; +export function arrayContainsAll( + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { + // @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): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; +export function arrayLength(array: Expression | string): FunctionExpression { + 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' + * equalAny(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 equalAny( + expression: Expression, + values: Array +): BooleanExpression; + +/** + * @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 + * equalAny(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 equalAny( + expression: Expression, + arrayExpression: Expression +): BooleanExpression; + +/** + * @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' + * equalAny("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 equalAny( + fieldName: string, + values: Array +): BooleanExpression; + +/** + * @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' + * equalAny("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 equalAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; +export function equalAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { + // @ts-ignore implementation accepts both types + return fieldOrExpression(element).equalAny(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' + * notEqualAny(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 notEqualAny( + element: Expression, + values: Array +): BooleanExpression; + +/** + * @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' + * notEqualAny("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 notEqualAny( + fieldName: string, + values: Array +): BooleanExpression; + +/** + * @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' + * notEqualAny(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 notEqualAny( + element: Expression, + arrayExpression: Expression +): BooleanExpression; + +/** + * @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' + * notEqualAny("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 notEqualAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; + +export function notEqualAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { + // @ts-ignore implementation accepts both types + return fieldOrExpression(element).notEqualAny(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( + * greaterThan("age", 18), + * equal("city", "London"), + * equal("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: BooleanExpression, + second: BooleanExpression, + ...additionalConditions: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression( + 'xor', + [first, second, ...additionalConditions], + 'xor' + ); +} + +/** + * @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". + * conditional( + * greaterThan("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 conditional( + condition: BooleanExpression, + thenExpr: Expression, + elseExpr: Expression +): FunctionExpression { + return new FunctionExpression( + 'conditional', + [condition, thenExpr, elseExpr], + 'conditional' + ); +} + +/** + * @beta + * + * Creates an expression that negates a filter condition. + * + * ```typescript + * // Find documents where the 'completed' field is NOT true + * not(equal("completed", true)); + * ``` + * + * @param booleanExpr The filter condition to negate. + * @return A new {@code Expr} representing the negated filter condition. + */ +export function not(booleanExpr: BooleanExpression): BooleanExpression { + 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 maximum operation. + */ +export function logicalMaximum( + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * @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 maximum operation. + */ +export function logicalMaximum( + fieldName: string, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +export function logicalMaximum( + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { + 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 minimum operation. + */ +export function logicalMinimum( + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * @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 minimum operation. + */ +export function logicalMinimum( + fieldName: string, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +export function logicalMinimum( + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function exists(valueOrField: Expression | string): BooleanExpression { + 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: Expression): BooleanExpression; + +/** + * @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): BooleanExpression; +export function isNan(value: Expression | string): BooleanExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function reverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).reverse(); +} + +/** + * @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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function byteLength(expr: Expression | string): FunctionExpression { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.byteLength(); +} + +/** + * Creates an expression that reverses an array. + * + * ```typescript + * // Reverse the value of the 'myArray' field. + * arrayReverse("myArray"); + * ``` + * + * @param fieldName The name of the field to reverse. + * @return A new {@code Expr} representing the reversed array. + */ +export function arrayReverse(fieldName: string): FunctionExpression; + +/** + * Creates an expression that reverses an array. + * + * ```typescript + * // Reverse the value of the 'myArray' field. + * arrayReverse(field("myArray")); + * ``` + * + * @param arrayExpression An expression evaluating to an array value, which will be reversed. + * @return A new {@code Expr} representing the reversed array. + */ +export function arrayReverse(arrayExpression: Expression): FunctionExpression; +export function arrayReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arrayReverse(); +} + +/** + * Creates an expression that computes e to the power of the expression's result. + * + * ```typescript + * // Compute e to the power of 2. + * exp(constant(2)); + * ``` + * + * @return A new {@code Expr} representing the exp of the numeric value. + */ +export function exp(expression: Expression): FunctionExpression; + +/** + * Creates an expression that computes e to the power of the expression's result. + * + * ```typescript + * // Compute e to the power of the 'value' field. + * exp('value'); + * ``` + * + * @return A new {@code Expr} representing the exp of the numeric value. + */ +export function exp(fieldName: string): FunctionExpression; + +export function exp( + expressionOrFieldName: Expression | string +): FunctionExpression { + return fieldOrExpression(expressionOrFieldName).exp(); +} + +/** + * Creates an expression that computes the ceiling of a numeric value. + * + * ```typescript + * // Compute the ceiling of the 'price' field. + * ceil("price"); + * ``` + * + * @param fieldName The name of the field to compute the ceiling of. + * @return A new {@code Expr} representing the ceiling of the numeric value. + */ +export function ceil(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the ceiling of a numeric value. + * + * ```typescript + * // Compute the ceiling of the 'price' field. + * ceil(field("price")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the ceiling will be computed for. + * @return A new {@code Expr} representing the ceiling of the numeric value. + */ +export function ceil(expression: Expression): FunctionExpression; +export function ceil(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ceil(); +} + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param expr The expression to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param fieldName The name of the field to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(fieldName: string): FunctionExpression; +export function floor(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).floor(); +} + +/** + * Creates an aggregation that counts the number of distinct values of a field. + * + * @param expr The expression or field to count distinct values of. + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ +export function countDistinct(expr: Expression | string): AggregateFunction { + return fieldOrExpression(expr).countDistinct(); +} + +/** + * @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): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; +export function charLength(value: Expression | string): FunctionExpression { + 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): BooleanExpression; + +/** + * @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: Expression): BooleanExpression; + +/** + * @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: Expression, + pattern: string +): BooleanExpression; + +/** + * @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: Expression, + pattern: Expression +): BooleanExpression; +export function like( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { + 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 +): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; + +/** + * @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: Expression, + pattern: string +): BooleanExpression; + +/** + * @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: Expression, + pattern: Expression +): BooleanExpression; +export function regexContains( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { + 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 +): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; + +/** + * @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: Expression, + pattern: string +): BooleanExpression; + +/** + * @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: Expression, + pattern: Expression +): BooleanExpression; +export function regexMatch( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { + 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". + * stringContains("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 stringContains( + fieldName: string, + substring: string +): BooleanExpression; + +/** + * @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. + * stringContains("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 stringContains( + fieldName: string, + substring: Expression +): BooleanExpression; + +/** + * @beta + * + * Creates an expression that checks if a string expression contains a specified substring. + * + * ```typescript + * // Check if the 'description' field contains "example". + * stringContains(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 stringContains( + stringExpression: Expression, + substring: string +): BooleanExpression; + +/** + * @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. + * stringContains(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 stringContains( + stringExpression: Expression, + substring: Expression +): BooleanExpression; +export function stringContains( + left: Expression | string, + substring: Expression | string +): BooleanExpression { + const leftExpr = fieldOrExpression(left); + const substringExpr = valueToDefaultExpr(substring); + return leftExpr.stringContains(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 +): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; + +/** + * @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: Expression, + prefix: string +): BooleanExpression; + +/** + * @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: Expression, + prefix: Expression +): BooleanExpression; +export function startsWith( + expr: Expression | string, + prefix: Expression | string +): BooleanExpression { + 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): BooleanExpression; + +/** + * @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: Expression +): BooleanExpression; + +/** + * @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: Expression, + suffix: string +): BooleanExpression; + +/** + * @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: Expression, + suffix: Expression +): BooleanExpression; +export function endsWith( + expr: Expression | string, + suffix: Expression | string +): BooleanExpression { + 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): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; +export function toLower(expr: Expression | string): FunctionExpression { + 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): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; +export function toUpper(expr: Expression | string): FunctionExpression { + 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): FunctionExpression; + +/** + * @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: Expression): FunctionExpression; +export function trim(expr: Expression | string): FunctionExpression { + 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 + * stringConcat("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 stringConcat( + fieldName: string, + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; + +/** + * @beta + * Creates an expression that concatenates string expressions together. + * + * ```typescript + * // Combine the 'firstName', " ", and 'lastName' fields into a single string + * stringConcat(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 stringConcat( + firstString: Expression, + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; +export function stringConcat( + first: string | Expression, + second: string | Expression, + ...elements: Array +): FunctionExpression { + return fieldOrExpression(first).stringConcat( + 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): FunctionExpression; + +/** + * @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: Expression, + subField: string +): FunctionExpression; +export function mapGet( + fieldOrExpr: string | Expression, + subField: string +): FunctionExpression { + 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', [], '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").greaterThan(10)).as("expensiveItemCount"); + * ``` + * + * @param expression The expression to count. + * @return A new {@code AggregateFunction} representing the 'count' aggregation. + */ +export function count(expression: Expression): 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: Expression | 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: Expression): 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: Expression | 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 + * average(field("age")).as("averageAge"); + * ``` + * + * @param expression The expression representing the values to average. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. + */ +export function average(expression: Expression): 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 + * average("age").as("averageAge"); + * ``` + * + * @param fieldName The name of the field containing numeric values to average. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. + */ +export function average(fieldName: string): AggregateFunction; +export function average(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).average(); +} + +/** + * @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 'minimum' aggregation. + */ +export function minimum(expression: Expression): 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 'minimum' aggregation. + */ +export function minimum(fieldName: string): AggregateFunction; +export function minimum(value: Expression | 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 'maximum' aggregation. + */ +export function maximum(expression: Expression): 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 'maximum' aggregation. + */ +export function maximum(fieldName: string): AggregateFunction; +export function maximum(value: Expression | 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 +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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: Expression, + vector: number[] | VectorValue +): FunctionExpression; + +/** + * @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: Expression, + otherVectorExpression: Expression +): FunctionExpression; +export function cosineDistance( + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { + 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 +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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: Expression, + vector: number[] | VectorValue +): FunctionExpression; + +/** + * @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: Expression, + otherVectorExpression: Expression +): FunctionExpression; +export function dotProduct( + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { + 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 +): FunctionExpression; + +/** + * @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: Expression +): FunctionExpression; + +/** + * @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: Expression, + vector: number[] | VectorValue +): FunctionExpression; + +/** + * @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: Expression, + otherVectorExpression: Expression +): FunctionExpression; +export function euclideanDistance( + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function vectorLength(expr: Expression | string): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function unixMicrosToTimestamp( + expr: Expression | string +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function timestampToUnixMicros( + expr: Expression | string +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function unixMillisToTimestamp( + expr: Expression | string +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function timestampToUnixMillis( + expr: Expression | string +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function unixSecondsToTimestamp( + expr: Expression | string +): FunctionExpression { + 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: Expression): FunctionExpression; + +/** + * @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): FunctionExpression; +export function timestampToUnixSeconds( + expr: Expression | string +): FunctionExpression { + 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: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; + +/** + * @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: Expression, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpression; + +/** + * @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 +): FunctionExpression; +export function timestampAdd( + timestamp: Expression | string, + unit: + | Expression + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expression | number +): FunctionExpression { + 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. + * timestampSubtract(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 timestampSubtract( + timestamp: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; + +/** + * @beta + * + * Creates an expression that subtracts a specified amount of time from a timestamp. + * + * ```typescript + * // Subtract 1 day from the 'timestamp' field. + * timestampSubtract(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 timestampSubtract( + timestamp: Expression, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpression; + +/** + * @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. + * timestampSubtract("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 timestampSubtract( + fieldName: string, + unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', + amount: number +): FunctionExpression; +export function timestampSubtract( + timestamp: Expression | string, + unit: + | Expression + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day', + amount: Expression | number +): FunctionExpression { + const normalizedTimestamp = fieldOrExpression(timestamp); + const normalizedUnit = valueToDefaultExpr(unit); + const normalizedAmount = valueToDefaultExpr(amount); + return normalizedTimestamp.timestampSubtract( + normalizedUnit, + normalizedAmount + ); +} + +/** + * @beta + * + * Creates an expression that evaluates to the current server timestamp. + * + * ```typescript + * // Get the current server timestamp + * currentTimestamp() + * ``` + * + * @return A new Expression representing the current server timestamp. + */ +export function currentTimestamp(): FunctionExpression { + return new FunctionExpression('current_timestamp', [], 'currentTimestamp'); +} + +/** + * Creates an expression that raises an error with the given message. This could be useful for + * debugging purposes. + * + * ```typescript + * // Raise an error with the message "simulating an evaluation error". + * error("simulating an evaluation error") + * ``` + * + * @return A new Expression representing the error() operation. + */ +export function error(message: string): Expression { + return new FunctionExpression( + 'error', + [constant(message)], + 'currentTimestamp' + ); +} + +/** + * @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(greaterThan("age", 18), equal("city", "London"), equal("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: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('and', [first, second, ...more], 'and'); +} + +/** + * @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(greaterThan("age", 18), equal("city", "London"), equal("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: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('or', [first, second, ...more], 'xor'); +} + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow(field("base"), field("exponent")); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow(field("base"), 2); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: number): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow("base", field("exponent")); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow("base", 2); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: number): FunctionExpression; +export function pow( + base: Expression | string, + exponent: Expression | number +): FunctionExpression { + return fieldOrExpression(base).pow(exponent as number); +} + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round("price"); + * ``` + * + * @param fieldName The name of the field to round. + * @return A new `Expr` representing the rounded value. + */ +export function round(fieldName: string): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round(field("price")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @return A new `Expr` representing the rounded value. + */ +export function round(expression: Expression): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round("price", 2); + * ``` + * + * @param fieldName The name of the field to round. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + fieldName: string, + decimalPlaces: number | Expression +): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round(field("price"), constant(2)); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + expression: Expression, + decimalPlaces: number | Expression +): FunctionExpression; +export function round( + expr: Expression | string, + decimalPlaces?: number | Expression +): FunctionExpression { + if (decimalPlaces === undefined) { + return fieldOrExpression(expr).round(); + } else { + return fieldOrExpression(expr).round(valueToDefaultExpr(decimalPlaces)); + } +} + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId("__name__"); + * ``` + * + * @param fieldName The name of the field to get the collection ID from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(fieldName: string): FunctionExpression; + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId(field("__name__")); + * ``` + * + * @param expression An expression evaluating to a path, which the collection ID will be extracted from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(expression: Expression): FunctionExpression; +export function collectionId(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).collectionId(); +} + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length("name"); + * + * // Get the number of items in the 'cart' array. + * length("cart"); + * ``` + * + * @param fieldName The name of the field to calculate the length of. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(fieldName: string): FunctionExpression; + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length(field("name")); + * + * // Get the number of items in the 'cart' array. + * length(field("cart")); + * ``` + * + * @param expression An expression evaluating to a string, array, map, vector, or bytes, which the length will be calculated for. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(expression: Expression): FunctionExpression; +export function length(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).length(); +} + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln("value"); + * ``` + * + * @param fieldName The name of the field to compute the natural logarithm of. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the natural logarithm will be computed for. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(expression: Expression): FunctionExpression; +export function ln(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ln(); +} + +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log(field("value"), 10); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(expression: Expression, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log(field("value"), field("base")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log( + expression: Expression, + base: Expression +): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log("value", 10); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log("value", field("base")); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: Expression): FunctionExpression; +export function log( + expr: Expression | string, + base: number | Expression +): FunctionExpression { + return new FunctionExpression('log', [ + fieldOrExpression(expr), + valueToDefaultExpr(base) + ]); +} + +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the square root will be computed for. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(expression: Expression): FunctionExpression; +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt("value"); + * ``` + * + * @param fieldName The name of the field to compute the square root of. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(fieldName: string): FunctionExpression; +export function sqrt(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).sqrt(); +} + +/** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse(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 stringReverse(stringExpression: Expression): FunctionExpression; + +/** + * Creates an expression that reverses a string value in the specified field. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse("myString"); + * ``` + * + * @param field The name of the field representing the string to reverse. + * @return A new {@code Expr} representing the reversed string. + */ +export function stringReverse(field: string): FunctionExpression; +export function stringReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).stringReverse(); +} + +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate the 'firstName' and 'lastName' fields with a space in between. + * concat(field("firstName"), " ", field("lastName")) + * ``` + * + * @param first The first expressions to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literals or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate a field with a literal string. + * concat(field("firstName"), "Doe") + * ``` + * + * @param fieldName The name of a field to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literal or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + fieldName: string, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +export function concat( + fieldNameOrExpression: string | Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression { + return new FunctionExpression('concat', [ + fieldOrExpression(fieldNameOrExpression), + valueToDefaultExpr(second), + ...others.map(valueToDefaultExpr) + ]); +} + +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param expr The expression to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param fieldName The field to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(fieldName: string): FunctionExpression; +export function abs(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).abs(); +} + +/** + * Creates an expression that returns the `elseExpr` argument if `ifExpr` is absent, else return + * the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), constant("default_value")) + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifExpr` is absent, else + * return the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), "default_value") + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseValue The value that will be returned if `ifExpr` evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; + +/** + * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns the value of + * // 'default_field' if 'optional_field' is absent. + * ifAbsent("optional_field", field("default_field")) + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseExpr The expression that will be evaluated and returned if `ifFieldName` is + * absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent("optional_field", "default_value") + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseValue The value that will be returned if [ifFieldName] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent( + ifFieldName: string | Expression, + elseValue: Expression | unknown +): Expression; +export function ifAbsent( + fieldNameOrExpression: string | Expression, + elseValue: Expression | unknown +): Expression { + return fieldOrExpression(fieldNameOrExpression).ifAbsent( + valueToDefaultExpr(elseValue) + ); +} + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join("tags", ", ") + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join(arrayFieldName: string, delimiter: string): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join an array of string using the delimiter from the 'separator' field. + * join(array(['foo', 'bar']), field("separator")) + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join( + arrayExpression: Expression, + delimiterExpression: Expression +): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join(field("tags"), ", ") + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join( + arrayExpression: Expression, + delimiter: string +): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * join('tags', field("separator")) + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join( + arrayFieldName: string, + delimiterExpression: Expression +): Expression; +export function join( + fieldNameOrExpression: string | Expression, + delimiterValueOrExpression: Expression | string +): Expression { + return fieldOrExpression(fieldNameOrExpression).join( + valueToDefaultExpr(delimiterValueOrExpression) + ); +} + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10("value"); + * ``` + * + * @param fieldName The name of the field to compute the base-10 logarithm of. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the base-10 logarithm will be computed for. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(expression: Expression): FunctionExpression; +export function log10(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).log10(); +} + +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum("scores"); + * ``` + * + * @param fieldName The name of the field to compute the sum of. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum(field("scores")); + * ``` + * + * @param expression An expression evaluating to a numeric array, which the sum will be computed for. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(expression: Expression): FunctionExpression; +export function arraySum(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arraySum(); +} + +// TODO(new-expression): Add new top-level expression function definitions above this line + +/** + * @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: Expression): 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: Expression | string): Ordering { + return new Ordering(fieldOrExpression(field), 'ascending', '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: Expression): 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: Expression | string): Ordering { + return new Ordering(fieldOrExpression(field), 'descending', '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( + public readonly expr: Expression, + public readonly direction: 'ascending' | 'descending', + readonly _methodName: string | undefined + ) {} + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + mapValue: { + fields: { + direction: toStringValue(this.direction), + expression: this.expr._toProto(serializer) + } + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); + } + + _protoValueType: 'ProtoValue' = 'ProtoValue'; +} + +export function isSelectable(val: unknown): val is Selectable { + const candidate = val as Selectable; + return ( + candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) + ); +} + +export function isOrdering(val: unknown): val is Ordering { + const candidate = val as Ordering; + return ( + isExpr(candidate.expr) && + (candidate.direction === 'ascending' || + candidate.direction === 'descending') + ); +} + +export function isAliasedAggregate(val: unknown): val is AliasedAggregate { + const candidate = val as AliasedAggregate; + return ( + isString(candidate.alias) && + candidate.aggregate instanceof AggregateFunction + ); +} + +export function isExpr(val: unknown): val is Expression { + return val instanceof Expression; +} + +export function isBooleanExpr(val: unknown): val is BooleanExpression { + return val instanceof BooleanExpression; +} + +export function isField(val: unknown): val is Field { + return val instanceof Field; +} + +export function toField(value: string | Field): Field { + if (isString(value)) { + const result = field(value); + return result; + } else { + return value as Field; + } +} 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..c352ba48338 --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -0,0 +1,228 @@ +/** + * @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, isField } from './expressions'; +import { FieldPath } from './field_path'; +import { Pipeline } from './pipeline'; +import { DocumentData, DocumentReference, refEqual } from './reference'; +import { Timestamp } from './timestamp'; +import { fieldPathFromArgument } from './user_data_reader'; +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; + } + + /** 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; + + /** + * @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. + * @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, + fields: ObjectValue, + ref?: DocumentReference, + 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 {T} 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 { + 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; + } + if (isField(fieldPath)) { + fieldPath = fieldPath.fieldName; + } + + 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..3f4d62cb0be --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -0,0 +1,265 @@ +/** + * @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 { Code, FirestoreError } from '../util/error'; +import { isString } from '../util/types'; + +import { Pipeline } from './pipeline'; +import { + CollectionReference, + DocumentReference, + isCollectionReference, + Query +} from './reference'; +import { + CollectionGroupSource, + CollectionSource, + DatabaseSource, + DocumentsSource, + Stage +} from './stage'; +import { + CollectionGroupStageOptions, + CollectionStageOptions, + DatabaseStageOptions, + DocumentsStageOptions +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; + +/** + * Represents the source of a Firestore {@link Pipeline}. + * @beta + */ +export class PipelineSource { + /** + * @internal + * @private + * @param databaseId + * @param userDataReader + * @param _createPipeline + */ + constructor( + private databaseId: DatabaseId, + private userDataReader: UserDataReader, + /** + * @internal + * @private + */ + public _createPipeline: (stages: Stage[]) => PipelineType + ) {} + + /** + * Returns all documents from the entire collection. The collection can be nested. + * @param collection - Name or reference to the collection that will be used as the Pipeline source. + */ + collection(collection: string | CollectionReference): PipelineType; + /** + * Returns all documents from the entire collection. The collection can be nested. + * @param options - Options defining how this CollectionStage is evaluated. + */ + collection(options: CollectionStageOptions): PipelineType; + collection( + collectionOrOptions: string | CollectionReference | CollectionStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + const options = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? {} + : collectionOrOptions; + const collectionRefOrString = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? collectionOrOptions + : collectionOrOptions.collection; + + // Validate that a user provided reference is for the same Firestore DB + if (isCollectionReference(collectionRefOrString)) { + this._validateReference(collectionRefOrString); + } + + // Convert user land convenience types to internal types + const normalizedCollection = isString(collectionRefOrString) + ? (collectionRefOrString as string) + : collectionRefOrString.path; + + // Create stage object + const stage = new CollectionSource(normalizedCollection, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collection' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); + } + + /** + * Returns all documents from a collection ID regardless of the parent. + * @param collectionId - ID of the collection group to use as the Pipeline source. + */ + collectionGroup(collectionId: string): PipelineType; + /** + * Returns all documents from a collection ID regardless of the parent. + * @param options - Options defining how this CollectionGroupStage is evaluated. + */ + collectionGroup(options: CollectionGroupStageOptions): PipelineType; + collectionGroup( + collectionIdOrOptions: string | CollectionGroupStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let collectionId: string; + let options: {}; + if (isString(collectionIdOrOptions)) { + collectionId = collectionIdOrOptions; + options = {}; + } else { + ({ collectionId, ...options } = collectionIdOrOptions); + } + + // Create stage object + const stage = new CollectionGroupSource(collectionId, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collectionGroup' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); + } + + /** + * Returns all documents from the entire database. + */ + database(): PipelineType; + /** + * Returns all documents from the entire database. + * @param options - Options defining how a DatabaseStage is evaluated. + */ + database(options: DatabaseStageOptions): PipelineType; + database(options?: DatabaseStageOptions): PipelineType { + // Process argument union(s) from method overloads + options = options ?? {}; + + // Create stage object + const stage = new DatabaseSource(options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'database' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); + } + + /** + * 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; + + /** + * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. + * + * @param options - Options defining how this DocumentsStage is evaluated. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + documents(options: DocumentsStageOptions): PipelineType; + documents( + docsOrOptions: Array | DocumentsStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let options: {}; + let docs: Array; + if (Array.isArray(docsOrOptions)) { + docs = docsOrOptions; + options = {}; + } else { + ({ docs, ...options } = docsOrOptions); + } + + // Validate that all user provided references are for the same Firestore DB + docs + .filter(v => v instanceof DocumentReference) + .forEach(dr => this._validateReference(dr as DocumentReference)); + + // Convert user land convenience types to internal types + const normalizedDocs: string[] = docs.map(doc => + isString(doc) ? doc : doc.path + ); + + // Create stage object + const stage = new DocumentsSource(normalizedDocs, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'documents' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); + } + + /** + * 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..f4aae07b828 --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -0,0 +1,1441 @@ +/** + * @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 ProtoPipeline, + Stage as ProtoStage +} from '../protos/firestore_proto_api'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; +import { isPlainObject } from '../util/input_validation'; +import { + aliasedAggregateToMap, + fieldOrExpression, + selectablesToMap, + vectorToExpr +} from '../util/pipeline_util'; +import { isNumber, isString } from '../util/types'; + +import { Firestore } from './database'; +import { + _mapValue, + AggregateFunction, + AliasedAggregate, + BooleanExpression, + _constant, + Expression, + Field, + field, + Ordering, + Selectable, + _field, + isSelectable, + isField, + isBooleanExpr, + isAliasedAggregate, + toField, + isOrdering, + isExpr +} from './expressions'; +import { + AddFields, + Aggregate, + Distinct, + FindNearest, + RawStage, + Limit, + Offset, + RemoveFields, + Replace, + Sample, + Select, + Sort, + Stage, + Union, + Unnest, + Where +} from './stage'; +import { + AddFieldsStageOptions, + AggregateStageOptions, + DistinctStageOptions, + FindNearestStageOptions, + LimitStageOptions, + OffsetStageOptions, + RemoveFieldsStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + SelectStageOptions, + SortStageOptions, + StageOptions, + UnionStageOptions, + UnnestStageOptions, + WhereStageOptions +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; +import { AbstractUserDataWriter } from './user_data_writer'; + +/** + * @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"))); + * ``` + */ +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 Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#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; + /** + * 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 Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#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 options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + addFields(options: AddFieldsStageOptions): Pipeline; + addFields( + fieldOrOptions: Selectable | AddFieldsStageOptions, + ...additionalFields: Selectable[] + ): Pipeline { + // Process argument union(s) from method overloads + let fields: Selectable[]; + let options: {}; + if (isSelectable(fieldOrOptions)) { + fields = [fieldOrOptions, ...additionalFields]; + options = {}; + } else { + ({ fields, ...options } = fieldOrOptions); + } + + // Convert user land convenience types to internal types + const normalizedFields: Map = selectablesToMap(fields); + + // Create stage object + const stage = new AddFields(normalizedFields, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + removeFields(options: RemoveFieldsStageOptions): Pipeline; + removeFields( + fieldValueOrOptions: Field | string | RemoveFieldsStageOptions, + ...additionalFields: Array + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? {} + : fieldValueOrOptions; + const fields: Array = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? [fieldValueOrOptions, ...additionalFields] + : fieldValueOrOptions.fields; + + // Convert user land convenience types to internal types + const convertedFields: Field[] = fields.map(f => + isString(f) ? field(f) : (f as Field) + ); + + // Create stage object + const stage = new RemoveFields(convertedFields, options); + + // User data must be read in the context of the API method to + // provide contextual errors + stage._readUserData( + this.userDataReader.createContext(UserDataSource.Argument, 'removeFields') + ); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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 Expression#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 + * db.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; + /** + * 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 Expression#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 + * db.pipeline().collection("books") + * .select( + * "firstName", + * field("lastName"), + * field("address").toUppercase().as("upperAddress"), + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + select(options: SelectStageOptions): Pipeline; + select( + selectionOrOptions: Selectable | string | SelectStageOptions, + ...additionalSelections: Array + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? {} + : selectionOrOptions; + + const selections: Array = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? [selectionOrOptions, ...additionalSelections] + : selectionOrOptions.selections; + + // Convert user land convenience types to internal types + const normalizedSelections: Map = + selectablesToMap(selections); + + // Create stage object + const stage = new Select(normalizedSelections, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'select' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpression}. + * + *

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 + * BooleanExpression}, 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 BooleanExpression} to apply. + * @return A new Pipeline object with this stage appended to the stage list. + */ + where(condition: BooleanExpression): Pipeline; + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpression}. + * + *

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 + * BooleanExpression}, 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 options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + where(options: WhereStageOptions): Pipeline; + where(conditionOrOptions: BooleanExpression | WhereStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isBooleanExpr(conditionOrOptions) ? {} : conditionOrOptions; + const condition: BooleanExpression = isBooleanExpr(conditionOrOptions) + ? conditionOrOptions + : conditionOrOptions.condition; + + // Create stage object + const stage = new Where(condition, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'where' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + offset(options: OffsetStageOptions): Pipeline; + offset(offsetOrOptions: number | OffsetStageOptions): Pipeline { + // Process argument union(s) from method overloads + let options: {}; + let offset: number; + if (isNumber(offsetOrOptions)) { + options = {}; + offset = offsetOrOptions; + } else { + options = offsetOrOptions; + offset = offsetOrOptions.offset; + } + + // Create stage object + const stage = new Offset(offset, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'offset' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + limit(options: LimitStageOptions): Pipeline; + limit(limitOrOptions: number | LimitStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(limitOrOptions) ? {} : limitOrOptions; + const limit: number = isNumber(limitOrOptions) + ? limitOrOptions + : limitOrOptions.limit; + + // Create stage object + const stage = new Limit(limit, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'limit' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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 Expression} 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 AliasedExpr}: Represents the result of a function with an assigned alias name + * using {@link Expression#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; + /** + * 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 Expression} 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 AliasedExpr}: Represents the result of a function with an assigned alias name + * using {@link Expression#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 options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + distinct(options: DistinctStageOptions): Pipeline; + distinct( + groupOrOptions: string | Selectable | DistinctStageOptions, + ...additionalGroups: Array + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? {} + : groupOrOptions; + const groups: Array = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? [groupOrOptions, ...additionalGroups] + : groupOrOptions.groups; + + // Convert user land convenience types to internal types + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Distinct(convertedGroups, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'distinct' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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 AliasedAggregate} expressions which are typically results of + * calling {@link Expression#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 AliasedAggregate}, wrapping an {@link AggregateFunction} + * and providing a name for the accumulated results. + * @param additionalAccumulators Optional additional {@link AliasedAggregate}, 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: AliasedAggregate, + ...additionalAccumulators: AliasedAggregate[] + ): 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 AliasedAggregate} expressions, which are typically created by + * calling {@link Expression#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 required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage + * list. + */ + aggregate(options: AggregateStageOptions): Pipeline; + aggregate( + targetOrOptions: AliasedAggregate | AggregateStageOptions, + ...rest: AliasedAggregate[] + ): Pipeline { + // Process argument union(s) from method overloads + const options = isAliasedAggregate(targetOrOptions) ? {} : targetOrOptions; + const accumulators: AliasedAggregate[] = isAliasedAggregate(targetOrOptions) + ? [targetOrOptions, ...rest] + : targetOrOptions.accumulators; + const groups: Array = isAliasedAggregate( + targetOrOptions + ) + ? [] + : targetOrOptions.groups ?? []; + + // Convert user land convenience types to internal types + const convertedAccumulators: Map = + aliasedAggregateToMap(accumulators); + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Aggregate( + convertedGroups, + convertedAccumulators, + options + ); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'aggregate' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * Performs a vector proximity search on the documents from the previous stage, returning the + * K-nearest documents based on the specified query `vectorValue` and `distanceMeasure`. The + * returned documents will be sorted in order from nearest to furthest from the query `vectorValue`. + * + *

Example: + * + * ```typescript + * // Find the 10 most similar books based on the book description. + * const bookDescription = "Lorem ipsum..."; + * const queryVector: number[] = ...; // compute embedding of `bookDescription` + * + * firestore.pipeline().collection("books") + * .findNearest({ + * field: 'embedding', + * vectorValue: queryVector, + * distanceMeasure: 'euclidean', + * limit: 10, // optional + * distanceField: 'computedDistance' // optional + * }); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + findNearest(options: FindNearestStageOptions): Pipeline { + // Convert user land convenience types to internal types + const field = toField(options.field); + const vectorValue = vectorToExpr(options.vectorValue); + const distanceField = options.distanceField + ? toField(options.distanceField) + : undefined; + const internalOptions = { + distanceField, + limit: options.limit, + rawOptions: options.rawOptions + }; + + // Create stage object + const stage = new FindNearest( + vectorValue, + field, + options.distanceMeasure, + internalOptions + ); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sort(options: SortStageOptions): Pipeline; + sort( + orderingOrOptions: Ordering | SortStageOptions, + ...additionalOrderings: Ordering[] + ): Pipeline { + // Process argument union(s) from method overloads + const options = isOrdering(orderingOrOptions) ? {} : orderingOrOptions; + const orderings: Ordering[] = isOrdering(orderingOrOptions) + ? [orderingOrOptions, ...additionalOrderings] + : orderingOrOptions.orderings; + + // Create stage object + const stage = new Sort(orderings, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sort' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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 Expression} that when returned evaluates to a map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(expr: Expression): 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 options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(options: ReplaceWithStageOptions): Pipeline; + replaceWith( + valueOrOptions: Expression | string | ReplaceWithStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isString(valueOrOptions) || isExpr(valueOrOptions) ? {} : valueOrOptions; + const fieldNameOrExpr: string | Expression = + isString(valueOrOptions) || isExpr(valueOrOptions) + ? valueOrOptions + : valueOrOptions.map; + + // Convert user land convenience types to internal types + const mapExpr = fieldOrExpression(fieldNameOrExpr); + + // Create stage object + const stage = new Replace(mapExpr, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'replaceWith' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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 - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sample(options: SampleStageOptions): Pipeline; + sample(documentsOrOptions: number | SampleStageOptions): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(documentsOrOptions) ? {} : documentsOrOptions; + let rate: number; + let mode: 'documents' | 'percent'; + if (isNumber(documentsOrOptions)) { + rate = documentsOrOptions; + mode = 'documents'; + } else if (isNumber(documentsOrOptions.documents)) { + rate = documentsOrOptions.documents; + mode = 'documents'; + } else { + rate = documentsOrOptions.percentage!; + mode = 'percent'; + } + + // Create stage object + const stage = new Sample(rate, mode, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sample' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + union(options: UnionStageOptions): Pipeline; + union(otherOrOptions: Pipeline | UnionStageOptions): Pipeline { + // Process argument union(s) from method overloads + let options: {}; + let otherPipeline: Pipeline; + if (isPipeline(otherOrOptions)) { + options = {}; + otherPipeline = otherOrOptions; + } else { + ({ other: otherPipeline, ...options } = otherOrOptions); + } + + // Create stage object + const stage = new Union(otherPipeline, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'union' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * 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; + /** + * 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 options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + unnest(options: UnnestStageOptions): Pipeline; + unnest( + selectableOrOptions: Selectable | UnnestStageOptions, + indexField?: string + ): Pipeline { + // Process argument union(s) from method overloads + let options: { indexField?: Field } & StageOptions; + let selectable: Selectable; + let indexFieldName: string | undefined; + if (isSelectable(selectableOrOptions)) { + options = {}; + selectable = selectableOrOptions; + indexFieldName = indexField; + } else { + ({ + selectable, + indexField: indexFieldName, + ...options + } = selectableOrOptions); + } + + // Convert user land convenience types to internal types + const alias = selectable.alias; + const expr = selectable.expr as Expression; + if (isString(indexFieldName)) { + options.indexField = _field(indexFieldName, 'unnest'); + } + + // Create stage object + const stage = new Unnest(alias, expr, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'unnest' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * Adds a raw stage to the pipeline. + * + *

This method provides a flexible way to extend the pipeline's functionality by adding custom + * stages. Each raw 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') + * .rawStage('where', [field('published').lt(1900)]) // Custom 'where' stage + * .select('title', 'author'); + * ``` + * + * @param name - The unique name of the raw stage to add. + * @param params - A list of parameters to configure the raw stage's behavior. + * @param options - An object of key value pairs that specifies optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + rawStage( + name: string, + params: unknown[], + options?: { [key: string]: Expression | unknown } + ): Pipeline { + // Convert user land convenience types to internal types + const expressionParams = params.map((value: unknown) => { + if (value instanceof Expression) { + return value; + } else if (value instanceof AggregateFunction) { + return value; + } else if (isPlainObject(value)) { + return _mapValue(value as Record); + } else { + return _constant(value, 'rawStage'); + } + }); + + // Create stage object + const stage = new RawStage(name, expressionParams, options ?? {}); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'rawStage' + ); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); + } + + /** + * @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 + ); + } + + /** + * @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); + } +} + +export function isPipeline(val: unknown): val is Pipeline { + return val instanceof Pipeline; +} 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..397e27bc1b4 --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -0,0 +1,133 @@ +/** + * @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 { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; +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, + UserDataReader, + UserDataSource +} 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); + + const udr = new UserDataReader( + pipeline._db._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + + const structuredPipelineOptions = new StructuredPipelineOptions({}, {}); + structuredPipelineOptions._readUserData(context); + + const structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + return invokeExecutePipeline(datastore, structuredPipeline).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.fields!, + element.key?.path + ? new DocumentReference(pipeline._db, null, element.key) + : undefined, + 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, + userDataReader, + (stages: Stage[]) => { + return new Pipeline(this, userDataReader, userDataWriter, stages); + } + ); +}; diff --git a/packages/firestore/src/lite-api/pipeline_options.ts b/packages/firestore/src/lite-api/pipeline_options.ts new file mode 100644 index 00000000000..f464428fd64 --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline_options.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 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 type { Pipeline } from './pipeline'; + +/** + * Options defining Pipeline execution. + */ +export interface PipelineExecuteOptions { + /** + * Pipeline to be evaluated. + */ + pipeline: Pipeline; + + /** + * Specify the index mode. + */ + indexMode?: 'recommended'; + + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Custom option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in rawOptions will take precedence over any options + * with the same name set by the SDK. + * + * Override the `example_option`: + * ``` + * execute({ + * pipeline: myPipeline, + * rawOptions: { + * // Override `example_option`. This will not + * // merge with the existing `example_option` object. + * "example_option": { + * foo: "bar" + * } + * } + * } + * ``` + * + * `rawOptions` supports dot notation, if you want to override + * a nested option. + * ``` + * execute({ + * pipeline: myPipeline, + * rawOptions: { + * // Override `example_option.foo` and do not override + * // any other properties of `example_option`. + * "example_option.foo": "bar" + * } + * } + * ``` + */ + rawOptions?: { + [name: string]: unknown; + }; +} diff --git a/packages/firestore/src/lite-api/query.ts b/packages/firestore/src/lite-api/query.ts index f0a357b828c..f019f0d0936 100644 --- a/packages/firestore/src/lite-api/query.ts +++ b/packages/firestore/src/lite-api/query.ts @@ -52,8 +52,9 @@ import { import { FieldPath } from './field_path'; import { DocumentData, DocumentReference, Query } from './reference'; -import { DocumentSnapshot, fieldPathFromArgument } from './snapshot'; +import { DocumentSnapshot } from './snapshot'; import { + fieldPathFromArgument, newUserDataReader, parseQueryValue, UserDataReader diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index f38dad9a078..43eedecf8b4 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -440,6 +440,12 @@ export class CollectionReference< } } +export function isCollectionReference( + val: unknown +): val is CollectionReference { + return val instanceof CollectionReference; +} + /** * Gets a `CollectionReference` instance that refers to the collection at * the specified absolute path. @@ -651,7 +657,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..ba7b08cf9dd 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -15,11 +15,10 @@ * limitations under the License. */ -import { Compat, getModularInstance } from '@firebase/util'; +import { getModularInstance } from '@firebase/util'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { FieldPath as InternalFieldPath } from '../model/path'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; @@ -34,7 +33,7 @@ import { WithFieldValue } from './reference'; import { - fieldPathFromDotSeparatedString, + fieldPathFromArgument, UntypedFirestoreDataConverter } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; @@ -509,19 +508,3 @@ export function snapshotEqual( return false; } - -/** - * Helper that calls `fromDotSeparatedString()` but wraps any error thrown. - */ -export function fieldPathFromArgument( - methodName: string, - arg: string | FieldPath | Compat -): InternalFieldPath { - if (typeof arg === 'string') { - return fieldPathFromDotSeparatedString(methodName, arg); - } else if (arg instanceof FieldPath) { - return arg._internalPath; - } 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..5dd30eedba7 --- /dev/null +++ b/packages/firestore/src/lite-api/stage.ts @@ -0,0 +1,764 @@ +/** + * @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 { OptionsUtil } from '../core/options_util'; +import { + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, + Stage as ProtoStage +} 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, + BooleanExpression, + Expression, + Field, + field, + Ordering +} from './expressions'; +import { Pipeline } from './pipeline'; +import { StageOptions } from './stage_options'; +import { isUserData, UserData } from './user_data_reader'; + +/** + * @beta + */ +export abstract class Stage implements ProtoSerializable, UserData { + /** + * Store optionsProto parsed by _readUserData. + * @private + * @internal + * @protected + */ + protected optionsProto: + | ApiClientObjectMap + | undefined = undefined; + protected knownOptions: Record; + protected rawOptions?: Record; + + constructor(options: StageOptions) { + ({ rawOptions: this.rawOptions, ...this.knownOptions } = options); + } + + _readUserData(context: ParseContext): void { + this.optionsProto = this._optionsUtil.getOptionsProto( + context, + this.knownOptions, + this.rawOptions + ); + } + + _toProto(_: JsonProtoSerializer): ProtoStage { + return { + name: this._name, + options: this.optionsProto + }; + } + + abstract get _optionsUtil(): OptionsUtil; + abstract get _name(): string; +} + +/** + * @beta + */ +export class AddFields extends Stage { + get _name(): string { + return 'add_fields'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private fields: Map, options: StageOptions) { + super(options); + } + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toMapValue(serializer, this.fields)] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } +} + +/** + * @beta + */ +export class RemoveFields extends Stage { + get _name(): string { + return 'remove_fields'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private fields: Field[], options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: this.fields.map(f => f._toProto(serializer)) + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } +} + +/** + * @beta + */ +export class Aggregate extends Stage { + get _name(): string { + return 'aggregate'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor( + private groups: Map, + private accumulators: Map, + options: StageOptions + ) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [ + toMapValue(serializer, this.accumulators), + toMapValue(serializer, this.groups) + ] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + readUserDataHelper(this.accumulators, context); + } +} + +/** + * @beta + */ +export class Distinct extends Stage { + get _name(): string { + return 'distinct'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private groups: Map, options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toMapValue(serializer, this.groups)] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + } +} + +/** + * @beta + */ +export class CollectionSource extends Stage { + get _name(): string { + return 'collection'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index' + } + }); + } + + private formattedCollectionPath: string; + + constructor(collection: string, options: StageOptions) { + super(options); + + // prepend slash to collection string + this.formattedCollectionPath = collection.startsWith('/') + ? collection + : '/' + collection; + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [{ referenceValue: this.formattedCollectionPath }] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class CollectionGroupSource extends Stage { + get _name(): string { + return 'collection_group'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index' + } + }); + } + + constructor(private collectionId: string, options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [{ referenceValue: '' }, { stringValue: this.collectionId }] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class DatabaseSource extends Stage { + get _name(): string { + return 'database'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer) + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class DocumentsSource extends Stage { + get _name(): string { + return 'documents'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + private formattedPaths: string[]; + + constructor(docPaths: string[], options: StageOptions) { + super(options); + this.formattedPaths = docPaths.map(path => + path.startsWith('/') ? path : '/' + path + ); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: this.formattedPaths.map(p => { + return { referenceValue: p }; + }) + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class Where extends Stage { + get _name(): string { + return 'where'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private condition: BooleanExpression, options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [this.condition._toProto(serializer)] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.condition, context); + } +} + +/** + * @beta + */ +export class FindNearest extends Stage { + get _name(): string { + return 'find_nearest'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + limit: { + serverName: 'limit' + }, + distanceField: { + serverName: 'distance_field' + } + }); + } + + constructor( + private vectorValue: Expression, + private field: Field, + private distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', + options: StageOptions + ) { + super(options); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [ + this.field._toProto(serializer), + this.vectorValue._toProto(serializer), + toStringValue(this.distanceMeasure) + ] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.vectorValue, context); + readUserDataHelper(this.field, context); + } +} + +/** + * @beta + */ +export class Limit extends Stage { + get _name(): string { + return 'limit'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private limit: number, options: StageOptions) { + hardAssert( + !isNaN(limit) && limit !== Infinity && limit !== -Infinity, + 0x882c, + 'Invalid limit value' + ); + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toNumber(serializer, this.limit)] + }; + } +} + +/** + * @beta + */ +export class Offset extends Stage { + get _name(): string { + return 'offset'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private offset: number, options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toNumber(serializer, this.offset)] + }; + } +} + +/** + * @beta + */ +export class Select extends Stage { + get _name(): string { + return 'select'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor( + private selections: Map, + options: StageOptions + ) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toMapValue(serializer, this.selections)] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.selections, context); + } +} + +/** + * @beta + */ +export class Sort extends Stage { + get _name(): string { + return 'sort'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private orderings: Ordering[], options: StageOptions) { + super(options); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: this.orderings.map(o => o._toProto(serializer)) + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.orderings, context); + } +} + +/** + * @beta + */ +export class Sample extends Stage { + get _name(): string { + return 'sample'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor( + private rate: number, + private mode: 'percent' | 'documents', + options: StageOptions + ) { + super(options); + } + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toNumber(serializer, this.rate)!, toStringValue(this.mode)!] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class Union extends Stage { + get _name(): string { + return 'union'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private other: Pipeline, options: StageOptions) { + super(options); + } + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [toPipelineValue(this.other._toProto(serializer))] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } +} + +/** + * @beta + */ +export class Unnest extends Stage { + get _name(): string { + return 'unnest'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + indexField: { + serverName: 'index_field' + } + }); + } + + constructor( + private alias: string, + private expr: Expression, + options: StageOptions + ) { + super(options); + } + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [ + this.expr._toProto(serializer), + field(this.alias)._toProto(serializer) + ] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.expr, context); + } +} + +/** + * @beta + */ +export class Replace extends Stage { + static readonly MODE = 'full_replace'; + + get _name(): string { + return 'replace_with'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } + + constructor(private map: Expression, options: StageOptions) { + super(options); + } + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + ...super._toProto(serializer), + args: [this.map._toProto(serializer), toStringValue(Replace.MODE)] + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.map, context); + } +} + +/** + * @beta + */ +export class RawStage extends Stage { + /** + * @private + * @internal + */ + constructor( + private name: string, + private params: Array, + rawOptions: Record + ) { + super({ rawOptions }); + } + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.params.map(o => o._toProto(serializer)), + options: this.optionsProto + }; + } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.params, context); + } + + get _name(): string { + return this.name; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } +} + +/** + * Helper to read user data across a number of different formats. + * @param name Name of the calling function. Used for error messages when invalid user data is encountered. + * @param expressionMap + * @return the expressionMap argument. + * @private + */ +function readUserDataHelper< + T extends Map | UserData[] | UserData +>(expressionMap: T, context: ParseContext): T { + if (isUserData(expressionMap)) { + expressionMap._readUserData(context); + } else if (Array.isArray(expressionMap)) { + expressionMap.forEach(readableData => readableData._readUserData(context)); + } else { + expressionMap.forEach(expr => expr._readUserData(context)); + } + return expressionMap; +} diff --git a/packages/firestore/src/lite-api/stage_options.ts b/packages/firestore/src/lite-api/stage_options.ts new file mode 100644 index 00000000000..828a81a2daa --- /dev/null +++ b/packages/firestore/src/lite-api/stage_options.ts @@ -0,0 +1,297 @@ +/** + * @license + * Copyright 2025 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 { OneOf } from '../util/types'; + +import { + AliasedAggregate, + BooleanExpression, + Expression, + Field, + Ordering, + Selectable +} from './expressions'; +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference } from './reference'; +import { VectorValue } from './vector_value'; + +/** + * Options defining how a Stage is evaluated. + */ +export interface StageOptions { + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Raw option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in rawOptions will take precedence over any options + * with the same name set by the SDK. + * + * `rawOptions` supports dot notation, if you want to override + * a nested option. + */ + rawOptions?: { + [name: string]: unknown; + }; +} +/** + * Options defining how a CollectionStage is evaluated. See {@link PipelineSource.collection}. + */ +export type CollectionStageOptions = StageOptions & { + /** + * Name or reference to the collection that will be used as the Pipeline source. + */ + collection: string | CollectionReference; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; + +/** + * Defines the configuration options for a {@link CollectionGroupStage} within a pipeline. + * This type extends {@link StageOptions} and provides specific settings for how a collection group + * is identified and processed during pipeline execution. + * + * @see {@link PipelineSource.collectionGroup} to create a collection group stage. + */ +export type CollectionGroupStageOptions = StageOptions & { + /** + * ID of the collection group to use as the Pipeline source. + */ + collectionId: string; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; +/** + * Options defining how a DatabaseStage is evaluated. See {@link PipelineSource.database}. + */ +export type DatabaseStageOptions = StageOptions & {}; +/** + * Options defining how a DocumentsStage is evaluated. See {@link PipelineSource.documents}. + */ +export type DocumentsStageOptions = StageOptions & { + /** + * 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. + * There must be at least one document specified in the array. + */ + docs: Array; +}; +/** + * Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}. + */ +export type AddFieldsStageOptions = StageOptions & { + /** + * The fields to add to each document, specified as a {@link Selectable}. + * At least one field is required. + */ + fields: Selectable[]; +}; +/** + * Options defining how a RemoveFieldsStage is evaluated. See {@link Pipeline.removeFields}. + */ +export type RemoveFieldsStageOptions = StageOptions & { + /** + * The fields to remove from each document. + */ + fields: Array; +}; +/** + * Options defining how a SelectStage is evaluated. See {@link Pipeline.select}. + */ +export type SelectStageOptions = StageOptions & { + /** + * The fields to include in the output documents, specified as {@link Selectable} expression + * or as a string value indicating the field name. + */ + selections: Array; +}; +/** + * Options defining how a WhereStage is evaluated. See {@link Pipeline.where}. + */ +export type WhereStageOptions = StageOptions & { + /** + * The {@link BooleanExpression} to apply as a filter for each input document to this stage. + */ + condition: BooleanExpression; +}; +/** + * Options defining how an OffsetStage is evaluated. See {@link Pipeline.offset}. + */ +export type OffsetStageOptions = StageOptions & { + /** + * The number of documents to skip. + */ + offset: number; +}; +/** + * Options defining how a LimitStage is evaluated. See {@link Pipeline.limit}. + */ +export type LimitStageOptions = StageOptions & { + /** + * The maximum number of documents to return. + */ + limit: number; +}; +/** + * Options defining how a DistinctStage is evaluated. See {@link Pipeline.distinct}. + */ +export type DistinctStageOptions = StageOptions & { + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups). + */ + groups: Array; +}; + +/** + * Options defining how an AggregateStage is evaluated. See {@link Pipeline.aggregate}. + */ +export type AggregateStageOptions = StageOptions & { + /** + * The {@link AliasedAggregate} values specifying aggregate operations to + * perform on the input documents. + */ + accumulators: AliasedAggregate[]; + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups), which will be aggregated over. + */ + groups?: Array; +}; +/** + * Options defining how a FindNearestStage is evaluated. See {@link Pipeline.findNearest}. + */ +export type FindNearestStageOptions = StageOptions & { + /** + * Specifies the field to be used. This can be a string representing the field path + * (e.g., 'fieldName', 'nested.fieldName') or an object of type {@link Field} + * representing a more complex field expression. + */ + field: Field | string; + /** + * Specifies the query vector value, to which the vector distance will be computed. + */ + vectorValue: VectorValue | number[]; + /** + * Specifies the method used to compute the distance between vectors. + * + * Possible values are: + * - `'euclidean'`: Euclidean distance. + * - `'cosine'`: Cosine similarity. + * - `'dot_product'`: Dot product. + */ + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + /** + * The maximum number of documents to return from the FindNearest stage. + */ + limit?: number; + /** + * If set, specifies the field on the output documents that will contain + * the computed vector distance for the document. If not set, the computed + * vector distance will not be returned. + */ + distanceField?: string; +}; +/** + * Options defining how a ReplaceWithStage is evaluated. See {@link Pipeline.replaceWith}. + */ +export type ReplaceWithStageOptions = StageOptions & { + /** + * The name of a field that contains a map or an {@link Expression} that + * evaluates to a map. + */ + map: Expression | string; +}; +/** + * Defines the options for evaluating a sample stage within a pipeline. + * This type combines common {@link StageOptions} with a specific configuration + * where only one of the defined sampling methods can be applied. + * + * See {@link Pipeline.sample} to create a sample stage.. + */ +export type SampleStageOptions = StageOptions & + OneOf<{ + /** + * If set, specifies the sample rate as a percentage of the + * input documents. + * + * Cannot be set when `documents: number` is set. + */ + percentage: number; + /** + * If set, specifies the sample rate as a total number of + * documents to sample from the input documents. + * + * Cannot be set when `percentage: number` is set. + */ + documents: number; + }>; +/** + * Options defining how a UnionStage is evaluated. See {@link Pipeline.union}. + */ +export type UnionStageOptions = StageOptions & { + /** + * Specifies the other Pipeline to union with. + */ + other: Pipeline; +}; + +/** + * Represents the specific options available for configuring an `UnnestStage` within a pipeline. + */ +export type UnnestStageOptions = StageOptions & { + /** + * A `Selectable` object that defines an array expression to be un-nested + * and the alias for the un-nested field. + */ + selectable: Selectable; + /** + * If set, specifies the field on the output documents that will contain the + * offset (starting at zero) that the element is from the original array. + */ + indexField?: string; +}; +/** + * Options defining how a SortStage is evaluated. See {@link Pipeline.sort}. + */ +export type SortStageOptions = StageOptions & { + /** + * Orderings specify how the input documents are sorted. + * One or more ordering are required. + */ + orderings: Ordering[]; +}; diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index a3022be627e..8ea2728b3a7 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)) { @@ -1101,3 +1079,11 @@ function fieldMaskContains( ): boolean { return haystack.some(v => v.isEqual(needle)); } + +export interface UserData { + _readUserData(context: ParseContext): void; +} + +export function isUserData(value: unknown): value is UserData { + return typeof (value as UserData)._readUserData === 'function'; +} 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/platform/rn_lite/snapshot_to_json.ts b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts index 709509c8a4e..0ccebd779f5 100644 --- a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts +++ b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts @@ -16,3 +16,10 @@ */ export { toByteStreamReader } from '../browser/byte_stream_reader'; + +// This is not included in the RN lite-bundle, but the rollup build +// will fail if these exports are not defined. +export { + buildDocumentSnapshotJsonBundle, + buildQuerySnapshotJsonBundle +} from '../browser/snapshot_to_json'; 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..081b8cf5c9a 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 { StructuredPipeline } from '../core/structured_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,44 @@ export async function invokeBatchGetDocumentsRpc( return result; } +export async function invokeExecutePipeline( + datastore: Datastore, + structuredPipeline: StructuredPipeline +): Promise { + const datastoreImpl = debugCast(datastore, DatastoreImpl); + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: getEncodedDatabaseId(datastoreImpl.serializer), + structuredPipeline: structuredPipeline._toProto(datastoreImpl.serializer) + }; + + const response = await datastoreImpl.invokeStreamingRPC< + ProtoExecutePipelineRequest, + ProtoExecutePipelineResponse + >( + 'ExecutePipeline', + datastoreImpl.serializer.databaseId, + ResourcePath.emptyPath(), + executePipelineRequest + ); + + const result: PipelineStreamElement[] = []; + response + .filter(proto => !!proto.results) + .forEach(proto => { + if (proto.results!.length === 0) { + result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); + } else { + return proto.results!.forEach(document => + result.push( + fromPipelineResponse(datastoreImpl.serializer, proto, document) + ) + ); + } + }); + + return 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..f11781ac331 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -37,6 +37,8 @@ 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 { TargetData, TargetPurpose } from '../local/target_data'; import { MutableDocument } from '../model/document'; @@ -55,6 +57,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 +90,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 +180,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 +438,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 +1452,98 @@ 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 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/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 7fd9967b5a0..f3b5dda6985 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { DocumentData } from '../lite-api/reference'; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; @@ -92,7 +93,7 @@ export function validateCollectionPath(path: ResourcePath): void { * Returns true if it's a non-null object without a custom prototype * (i.e. excludes Array, Date, etc.). */ -export function isPlainObject(input: unknown): boolean { +export function isPlainObject(input: unknown): input is DocumentData { return ( typeof input === 'object' && input !== null && diff --git a/packages/firestore/src/util/misc.ts b/packages/firestore/src/util/misc.ts index f2fa04d1b43..e2dc643783b 100644 --- a/packages/firestore/src/util/misc.ts +++ b/packages/firestore/src/util/misc.ts @@ -153,6 +153,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/pipeline_util.ts b/packages/firestore/src/util/pipeline_util.ts new file mode 100644 index 00000000000..0bc1906361c --- /dev/null +++ b/packages/firestore/src/util/pipeline_util.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2025 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 { vector } from '../api'; +import { + _constant, + AggregateFunction, + AliasedAggregate, + array, + constant, + Expression, + AliasedExpression, + field, + Field, + map, + Selectable +} from '../lite-api/expressions'; +import { VectorValue } from '../lite-api/vector_value'; + +import { isPlainObject } from './input_validation'; +import { isFirestoreValue } from './proto'; +import { isString } from './types'; + +export function 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 AliasedExpression) { + result.set(selectable.alias, selectable.expr); + } + } + return result; +} + +export function aliasedAggregateToMap( + aliasedAggregatees: AliasedAggregate[] +): Map { + return aliasedAggregatees.reduce( + (map: Map, selectable: AliasedAggregate) => { + map.set(selectable.alias, selectable.aggregate as AggregateFunction); + return map; + }, + new Map() as Map + ); +} + +/** + * 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 + */ +export function vectorToExpr( + value: VectorValue | number[] | Expression +): Expression { + if (value instanceof Expression) { + return value; + } else if (value instanceof VectorValue) { + const result = constant(value); + return result; + } else if (Array.isArray(value)) { + const result = constant(vector(value)); + return result; + } else { + throw new Error('Unsupported value: ' + typeof value); + } +} + +/** + * 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 + */ +export function fieldOrExpression(value: unknown): Expression { + if (isString(value)) { + const result = field(value); + return result; + } else { + return valueToDefaultExpr(value); + } +} +/** + * 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 + */ +export function valueToDefaultExpr(value: unknown): Expression { + let result: Expression | undefined; + if (isFirestoreValue(value)) { + return constant(value); + } + if (value instanceof Expression) { + return value; + } else if (isPlainObject(value)) { + result = map(value as Record); + } else if (value instanceof Array) { + result = array(value); + } else { + result = _constant(value, undefined); + } + + return result; +} 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..89ed50a240b 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -37,6 +37,10 @@ export function isNegativeZero(value: number): boolean { return value === 0 && 1 / value === 1 / -0; } +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + /** * Returns whether a value is an integer and in the safe integer range * @param value - The value to test for being an integer and in the safe range @@ -51,6 +55,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; @@ -65,3 +73,18 @@ export interface DocumentLike { addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; } + +/** + * Utility type to create an type that only allows one + * property of the Type param T to be set. + * + * type XorY = OneOf<{ x: unknown, y: unknown}> + * let a = { x: "foo" } // OK + * let b = { y: "foo" } // OK + * let c = { a: "foo", y: "foo" } // Not OK + */ +export type OneOf = { + [K in keyof T]: Pick & { + [P in Exclude]?: undefined; + }; +}[keyof T]; 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..11d13dbb07f --- /dev/null +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -0,0 +1,4170 @@ +/** + * @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 { FirebaseError } from '@firebase/util'; +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 { Deferred } from '../../util/promise'; +import { + GeoPoint, + Timestamp, + Bytes, + getFirestore, + terminate, + vector, + CollectionReference, + doc, + DocumentData, + Firestore, + setDoc, + setLogLevel, + collection, + documentId as documentIdFieldPath, + writeBatch, + addDoc, + DocumentReference, + deleteDoc +} from '../util/firebase_export'; +import { apiDescribe, withTestCollection } from '../util/helpers'; +import { + array, + mod, + pipelineResultEqual, + sum, + descending, + isNan, + map, + execute, + add, + arrayContainsAll, + unixSecondsToTimestamp, + and, + arrayContains, + arrayContainsAny, + count, + average, + cosineDistance, + not, + countAll, + dotProduct, + endsWith, + equal, + reverse, + toUpper, + euclideanDistance, + greaterThan, + like, + lessThan, + stringContains, + divide, + lessThanOrEqual, + arrayLength, + mapGet, + notEqual, + or, + regexContains, + regexMatch, + startsWith, + stringConcat, + subtract, + conditional, + equalAny, + logicalMaximum, + notEqualAny, + multiply, + countIf, + exists, + charLength, + minimum, + maximum, + isError, + ifError, + trim, + isAbsent, + isNull, + isNotNull, + isNotNan, + timestampSubtract, + mapRemove, + mapMerge, + documentId, + substring, + logicalMinimum, + xor, + field, + constant, + _internalPipelineToExecutePipelineRequestProto, + FindNearestStageOptions, + AggregateFunction, + arrayGet, + ascending, + BooleanExpression, + byteLength, + FunctionExpression, + timestampAdd, + timestampToUnixMicros, + timestampToUnixMillis, + timestampToUnixSeconds, + toLower, + unixMicrosToTimestamp, + unixMillisToTimestamp, + vectorLength, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + len as length, + abs, + concat, + error, + currentTimestamp, + ifAbsent, + join, + log10, + arraySum +} from '../util/pipeline_export'; + +use(chaiAsPromised); + +setLogLevel('debug'); + +const timestampDeltaMS = 1000; + +apiDescribe.only('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('console support', () => { + it('supports internal serialization to proto', async () => { + const pipeline = firestore + .pipeline() + .collection('books') + .where(equal('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; + }); + }); + + 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); + 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(average('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: [average('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); + }); + + it('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); + }); + + it('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(equal('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'), + constant(vector([1, 2, 3])).as('vectorValue'), + 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]), + '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( + equal('metadataArray', [ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]), + equal('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 + } + } + }); + }); + + it('supports boolean value constants as a BooleanExpression', async () => { + const snapshots = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + conditional(constant(true), constant('TRUE'), constant('FALSE')).as( + 'true' + ), + conditional( + constant(false), + constant('TRUE'), + constant('FALSE') + ).as('false') + ) + ); + + expectResults(snapshots, { + 'true': 'TRUE', + 'false': 'FALSE' + }); + }); + }); + + 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(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('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('supports aggregate options', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')] + }) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('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(lessThan('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(lessThan(field('published'), 1984)) + .aggregate({ + accumulators: [average('rating').as('avgRating')], + groups: ['genre'] + }) + .where(greaterThan('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').greaterThan(4.3)).as('count')) + ); + const expectedResults = { + count: 3 + }; + expectResults(snapshot, expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(field('rating').greaterThan(4.3).countIf().as('count')) + ); + expectResults(snapshot, expectedResults); + }); + + it('returns countDistinct accumulation', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countDistinct('genre').as('distinctGenres')) + ); + expectResults(snapshot, { distinctGenres: 8 }); + }); + }); + + 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' } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort({ + orderings: [ + 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' } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select({ selections: ['title', field('author').as('auth0r')] }) + .sort(field('auth0r').ascending()) + .limit(2) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + auth0r: 'Douglas Adams' + }, + { title: 'The Great Gatsby', auth0r: 'F. Scott Fitzgerald' } + ); + }); + }); + + 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' + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields({ + fields: [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" + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + .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('findNearest stage', () => { + it('can find nearest', 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" + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + .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( + greaterThan('rating', 4.5), + equalAny('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( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) + ) + ) + ); + expectResults(snapshot, 'book4'); + }); + + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('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( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('published', 1949) + ) + ) + .select('title') + ); + expectResults( + snapshot, + { title: 'Pride and Prejudice' }, + { title: 'The Lord of the Rings' }, + { title: "The Handmaid's Tale" } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where({ + condition: and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + }) + ); + expectResults(snapshot, 'book10', 'book4'); + }); + }); + + 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' } + ); + }); + + it('sort, offset, and limit stages support options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort({ + orderings: [field('author').ascending()] + }) + .offset({ offset: 5 }) + .limit({ 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('raw stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .rawStage('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') + .rawStage('add_fields', [ + { + display: stringConcat('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') + .rawStage('where', [field('author').equal('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') + .rawStage('sort', [ + { + direction: 'ascending', + expression: field('author') + } + ]) + .rawStage('offset', [3]) + .rawStage('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') + .rawStage('aggregate', [ + { averageRating: field('rating').average() }, + {} + ]) + ); + expectResults(snapshot, { + averageRating: 4.3100000000000005 + }); + }); + + it('can perform distinct query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .rawStage('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 + } + ); + }); + + it('can perform FindNearest query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .rawStage( + 'find_nearest', + [ + field('embedding'), + vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + 'euclidean' + ], + { + 'distance_field': field('computedDistance'), + limit: 2 + } + ) + .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('replaceWith stage', () => { + it('run pipeline with replaceWith field name', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('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(equal('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" } + }); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith({ map: 'awards' }) + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }); + }); + }); + + 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 = 30; + 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' + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union({ other: 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(equal('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 with index field', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag'), 'tagsIndex') + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); + 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 } }, + tagsIndex: 0 + }, + { + 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 } }, + tagsIndex: 1 + }, + { + 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 } }, + tagsIndex: 2 + } + ); + }); + + it('unnest an expr', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('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 } } + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest({ + selectable: field('tags').as('tag'), + indexField: 'tagsIndex' + }) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); + 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 } }, + tagsIndex: 0 + }, + { + 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 } }, + tagsIndex: 1 + }, + { + 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 } }, + tagsIndex: 2 + } + ); + }); + }); + + 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('error handling', () => { + it('error properties are propagated from the firestore backend', async () => { + try { + const myPipeline = firestore + .pipeline() + .collection(randomCol.path) + .rawStage('select', [ + // incorrect parameter type + field('title') + ]); + + await execute(myPipeline); + + expect.fail('expected pipeline.execute() to throw'); + } catch (e: unknown) { + expect(e instanceof FirebaseError).to.be.true; + const err = e as FirebaseError; + expect(err['code']).to.equal('invalid-argument'); + expect(typeof err['message']).to.equal('string'); + + expect(err['message']).to.match(/^3 INVALID_ARGUMENT: .*$/); + } + }); + }); + + 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('conditional works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + conditional( + lessThan(field('published'), 1960), + constant(1960), + field('published') + ).as('published-safe'), + field('rating') + .greaterThanOrEqual(4.5) + .conditional(constant('great'), constant('good')) + .as('rating') + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1960, rating: 'good' }, + { + title: 'Crime and Punishment', + 'published-safe': 1960, + rating: 'good' + }, + { title: 'Dune', 'published-safe': 1965, rating: 'great' } + ); + }); + + it('equalAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equalAny('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('notEqualAny works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + notEqualAny( + '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(equal('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').stringConcat(' - ', 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(stringContains('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(greaterThan('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(equal('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', 20).as('ratingTimes20'), + add('rating', 3).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( + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('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( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('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(divide(constant(1), constant(0))).as('isError'), + ifError(divide(constant(1), constant(0)), constant('was error')).as( + 'ifError' + ), + ifError( + divide(constant(1), constant(0)).greaterThan(1), + constant(true) + ) + .not() + .as('ifErrorBooleanExpression'), + 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', + ifErrorBooleanExpression: false, + 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'), + divide(constant(1), constant(0)).isError().as('isError'), + divide(constant(1), constant(0)) + .ifError(constant('was error')) + .as('ifError'), + divide(constant(1), constant(0)) + .greaterThan(1) + .ifError(constant(true)) + .not() + .as('ifErrorBooleanExpression'), + 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', + ifErrorBooleanExpression: false, + 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(equal('hugoAward', true)) + ); + expectResults( + snapshot, + { + hugoAward: true, + title: "The Hitchhiker's Guide to the Galaxy", + others: { unknown: { year: 1980 } } + }, + { hugoAward: true, title: 'Dune' } + ); + }); + + it('testDistanceFunctions', async () => { + const sourceVector = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + cosineDistance(constant(sourceVector), targetVector).as( + 'cosineDistance' + ), + dotProduct(constant(sourceVector), targetVector).as( + 'dotProductDistance' + ), + euclideanDistance(constant(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( + constant(sourceVector) + .cosineDistance(targetVector) + .as('cosineDistance'), + constant(sourceVector) + .dotProduct(targetVector) + .as('dotProductDistance'), + constant(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(constant(vector([1, 2, 3]))).as('vectorLength')) + ); + expectResults(snapshot, { + vectorLength: 3 + }); + }); + + it('testNestedFields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('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) + .limit(1) + .replaceWith( + map({ + title: 'foo', + nested: { + level: { + '1': 'bar' + }, + 'level.1': { + 'level.2': 'baz' + } + } + }) + ) + .select( + 'title', + field('nested.level.1'), + mapGet('nested', 'level.1').mapGet('level.2').as('nested') + ) + ); + expectResults(snapshot, { + title: 'foo', + 'nested.level.`1`': 'bar', + nested: 'baz' + }); + }); + + describe('rawFunction', () => { + it('add selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(descending('rating')) + .limit(1) + .select( + new FunctionExpression('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 BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(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 BooleanExpression('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').greaterThanOrEqual(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 FunctionExpression('char_length', [ + field('title') + ]).ascending(), + descending('__name__') + ) + .limit(3) + .select('title') + ); + expectResults( + snapshot, + { + title: '1984' + }, + { + title: 'Dune' + }, + { + title: 'The Great Gatsby' + } + ); + }); + }); + + 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 arrayGet', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(arrayGet('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').arrayGet(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'), + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('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) + }); + }).timeout(10000); + + 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(equal('trueField', true)).as('falseField')) + ); + + expectResults(snapshot, { + trueField: true, + falseField: false + }); + }); + + it('can reverse an array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('tags').arrayReverse().as('reversedTags')) + ); + expectResults(snapshot, { + reversedTags: ['adventure', 'space', 'comedy'] + }); + }); + + it('can reverse an array with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(reverse('tags').as('reversedTags')) + ); + expectResults(snapshot, { + reversedTags: ['adventure', 'space', 'comedy'] + }); + }); + + it('can compute the ceiling of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').ceil().as('ceilingRating')) + ); + expectResults(snapshot, { + ceilingRating: 5 + }); + }); + + it('can compute the ceiling of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ceil('rating').as('ceilingRating')) + ); + expectResults(snapshot, { + ceilingRating: 5 + }); + }); + + it('can compute the floor of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').floor().as('floorRating')) + ); + expectResults(snapshot, { + floorRating: 4 + }); + }); + + it('can compute the floor of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(floor('rating').as('floorRating')) + ); + expectResults(snapshot, { + floorRating: 4 + }); + }); + + it('can compute e to the power of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').exp().as('expRating')) + ); + expectResults(snapshot, { + expRating: 109.94717245212352 + }); + }); + + it('can compute e to the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(exp('rating').as('expRating')) + ); + expect(snapshot.results[0].get('expRating')).to.be.approximately( + 109.94717245212351, + 0.000001 + ); + }); + + it('can compute the power of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').pow(2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can compute the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(pow('rating', 2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value away from zero for positive half-way values', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .addFields(constant(1.5).as('positiveHalf')) + .select(field('positiveHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 2 + }); + }); + + it('can round a numeric value away from zero for negative half-way values', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .addFields(constant(-1.5).as('negativeHalf')) + .select(field('negativeHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: -2 + }); + }); + + it('can round a numeric value to specified precision', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + foo: 4.123456 + }) + ) + .select( + field('foo').round(0).as('0'), + round('foo', 1).as('1'), + round('foo', constant(2)).as('2'), + round(field('foo'), 4).as('4') + ) + ); + expectResults(snapshot, { + '0': 4, + '1': 4.1, + '2': 4.12, + '4': 4.1235 + }); + }); + + it('can get the collectionId from a path', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(field('__name__').collectionId().as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id + }); + }); + + it('can get the collectionId from a path with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(collectionId('__name__').as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id + }); + }); + + it('can compute the length of a string value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').length().as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of a string value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('title').as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of an array value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('tags').length().as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of an array value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('tags').as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of a map value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('awards').length().as('awardsLength')) + ); + expectResults(snapshot, { + awardsLength: 3 + }); + }); + + it('can compute the length of a vector value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('embedding').length().as('embeddingLength')) + ); + expectResults(snapshot, { + embeddingLength: 10 + }); + }); + + it('can compute the length of a bytes value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(constant('12é').as('value')) + .limit(1) + .select(field('value').byteLength().as('valueLength')) + ); + expectResults(snapshot, { + valueLength: 4 + }); + }); + + it('can compute the natural logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').ln().as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expectResults(snapshot, { + lnRating: 1.4350845252893227 + }); + }); + + it('can compute the logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log(field('rating'), 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can compute the logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log('rating', 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can compute the square root of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').sqrt().as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can compute the square root of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(sqrt('rating').as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can reverse a string', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').reverse().as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('can reverse a string with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(stringReverse('title').as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + documentId(field('__name__')).as('docId'), + documentId(field('__path__')).as('noDocId') + ) + ); + expectResults(snapshot, { + docId: 'book4', + noDocId: null + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('__name__').documentId().as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + }); + + it('supports substring', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('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').substring(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); + + it('supports substring without length', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('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').substring(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + }); + + it('test toLower', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .select(toLower('author').as('lowercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { + lowercaseAuthor: 'george orwell' + }); + }); + + it('test toUpper', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .select(toUpper('author').as('uppercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { uppercaseAuthor: 'GEORGE ORWELL' }); + }); + + it('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" + }); + }); + + it('test reverse', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', '1984')) + .limit(1) + .select(reverse('title').as('reverseTitle')) + ); + expectResults(snapshot, { reverseTitle: '4891' }); + }); + + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(-10).as('neg10'), + constant(-22.22).as('neg22'), + constant(1).as('pos1') + ) + .select( + abs('neg10').as('10'), + abs(field('neg22')).as('22'), + field('pos1').as('1') + ) + ); + expectResults(snapshot, { + '10': 10, + '22': 22.22, + '1': 1 + }); + }); + + it('can compute the base-10 logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').log10().as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can compute the base-10 logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(log10('rating').as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can concat fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + concat('author', ' ', field('title')).as('display'), + field('author').concat(': ', field('title')).as('display2') + ) + .where(equal('author', 'Douglas Adams')) + .select('display', 'display2') + ); + expectResults(snapshot, { + display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", + display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('supports currentTimestamp', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .addFields(currentTimestamp().as('now')) + .select('now') + ); + const now = snapshot.results[0].get('now') as Timestamp; + expect(now).instanceof(Timestamp); + expect( + now.toDate().getUTCSeconds() - new Date().getUTCSeconds() + ).lessThan(5000); + }); + + // Not implemented in backend + it.skip('supports error', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(isError(error('test error')).as('error')) + ); + + expectResults(snapshot, { + 'error': true + }); + }); + + it('supports ifAbsent', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + title: 'foo' + }) + ) + .select( + ifAbsent('title', 'default title').as('title'), + field('name').ifAbsent('default name').as('name'), + field('name').ifAbsent(field('title')).as('nameOrTitle') + ) + ); + + expectResults(snapshot, { + title: 'foo', + name: 'default name', + nameOrTitle: 'foo' + }); + }); + + it('supports join', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + tags: ['foo', 'bar', 'baz'], + delimeter: '|' + }) + ) + .select(join('tags', ',').as('csv'), field('tags').join('|').as('or')) + ); + + expectResults(snapshot, { + csv: 'foo,bar,baz', + or: 'foo|bar|baz' + }); + }); + + it('can compute the sum of the elements in an array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(field('sales').arraySum().as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + it('can compute the sum of the elements in an array with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(arraySum('sales').as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + // TODO(new-expression): Add new expression tests above this line + }); + + describe('pagination', () => { + let addedDocs: DocumentReference[] = []; + + /** + * 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 { + let docRef = doc(collectionReference, 'book11'); + addedDocs.push(docRef); + await setDoc(docRef, { + 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 } + }); + docRef = doc(collectionReference, 'book12'); + addedDocs.push(docRef); + await setDoc(docRef, { + 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: {} + }); + docRef = doc(collectionReference, 'book13'); + addedDocs.push(docRef); + await setDoc(docRef, { + 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 } + }); + } + + afterEach(async () => { + for (let i = 0; i < addedDocs.length; i++) { + await deleteDoc(addedDocs[i]); + } + addedDocs = []; + }); + + it('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: 'Dune', rating: 4.6 } + ); + + const lastDoc = snapshot.results[snapshot.results.length - 1]; + + snapshot = await execute( + pipeline + .where( + or( + and( + field('rating').equal(lastDoc.get('rating')), + field('__name__').greaterThan(lastDoc.ref) + ), + field('rating').lessThan(lastDoc.get('rating')) + ) + ) + .limit(pageSize) + ); + expectResults( + snapshot, + { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 }, + { title: 'The Master and Margarita', rating: 4.6 } + ); + }); + + it('supports pagination with offsets', async () => { + await addBooks(randomCol); + + const secondFilterField = '__name__'; + + 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('stage options', () => { + describe('forceIndex', () => { + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('Collection Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collection({ + collection: randomCol, + forceIndex: 'unknown' + }) + ); + expect(snapshot.results.length).to.equal(10); + }); + + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('CollectionGroup Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collectionGroup({ + collectionId: randomCol.id, + forceIndex: 'unknown' + }) + ); + expect(snapshot.results.length).to.equal(10); + }); + }); + }); +}); 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..1633492b578 --- /dev/null +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -0,0 +1,2843 @@ +/** + * @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, + constant, + add, + subtract, + multiply, + average, + substring, + count, + mapMerge, + mapRemove, + ifError, + isAbsent, + isError, + or, + isNotNan, + map, + isNotNull, + isNull, + mod, + documentId, + equal, + notEqual, + lessThan, + countIf, + lessThanOrEqual, + greaterThan, + arrayConcat, + arrayContains, + arrayContainsAny, + equalAny, + notEqualAny, + xor, + conditional, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + like, + regexContains, + regexMatch, + stringContains, + startsWith, + endsWith, + mapGet, + countAll, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + timestampToUnixSeconds, + timestampAdd, + timestampSubtract, + ascending, + descending, + FunctionExpression, + BooleanExpression, + AggregateFunction, + sum, + stringConcat, + arrayContainsAll, + arrayLength, + charLength, + divide, + abs, + not, + toLower, + toUpper, + trim, + arrayGet, + byteLength +} 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 { FindNearestStageOptions } from '../../src/lite-api/stage_options'; +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); + 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(average('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: [average('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(equal('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'), + constant(vector([1, 2, 3])).as('vectorValue'), + 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( + equal('metadataArray', [ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]), + equal('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(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('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(lessThan('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(lessThan(field('published'), 1984)) + .aggregate({ + accumulators: [average('rating').as('avgRating')], + groups: ['genre'] + }) + .where(greaterThan('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 minimum, maximum, 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').greaterThan(4.3)).as('count')) + ); + const expectedResults = { + count: 3 + }; + expectResults(snapshot, expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(field('rating').greaterThan(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( + greaterThan('rating', 4.5), + equalAny('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( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) + ) + ) + ); + expectResults(snapshot, 'book4'); + }); + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + or( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('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( + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('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('raw stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .rawStage('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') + .rawStage('add_fields', [ + { + display: stringConcat('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') + .rawStage('where', [field('author').equal('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') + .rawStage('sort', [ + { + direction: 'ascending', + expression: field('author') + } + ]) + .rawStage('offset', [3]) + .rawStage('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') + .rawStage('aggregate', [ + { averageRating: field('rating').average() }, + {} + ]) + ); + expectResults(snapshot, { + averageRating: 4.3100000000000005 + }); + }); + + it('can perform distinct query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .rawStage('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(equal('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(equal('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(equal('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(equal('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 maximum 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 minimum 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('conditiona works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + conditional( + lessThan(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(equalAny('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( + notEqualAny( + '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(equal('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').stringConcat(' - ', 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(stringContains('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(greaterThan('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(equal('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).as('ratingTimes20'), + add('rating', 1).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('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', 'To Kill a Mockingbird')) + .select(abs(field('rating')).as('absRating')) + .limit(1) + ); + expectResults(snapshot, { + absRating: 4.2 + }); + }); + + it('testComparisonOperators', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('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( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('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(arrayGet('title', 0)).as('isError'), + ifError(arrayGet('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'), + arrayGet('title', 0).isError().as('isError'), + arrayGet('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(equal('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 = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + cosineDistance(constant(sourceVector), targetVector).as( + 'cosineDistance' + ), + dotProduct(constant(sourceVector), targetVector).as( + 'dotProductDistance' + ), + euclideanDistance(constant(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( + constant(sourceVector) + .cosineDistance(targetVector) + .as('cosineDistance'), + constant(sourceVector) + .dotProduct(targetVector) + .as('dotProductDistance'), + constant(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(constant(vector([1, 2, 3]))).as('vectorLength')) + ); + expectResults(snapshot, { + vectorLength: 3 + }); + }); + + it('testNestedFields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('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(equal('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 FunctionExpression('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 BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(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 BooleanExpression('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').greaterThanOrEqual(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 FunctionExpression('char_length', [ + field('title') + ]).ascending(), + descending('__name__') + ) + .limit(3) + .select('title') + ); + expectResults( + snapshot, + { + title: '1984' + }, + { + title: 'Dune' + }, + { + title: 'The Great Gatsby' + } + ); + }); + }); + + 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(arrayGet('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').arrayGet(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'), + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('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) + }); + }).timeout(10000); + + 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(equal('trueField', true)).as('falseField')) + ); + + expectResults(snapshot, { + trueField: true, + falseField: false + }); + }); + + it('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' + }); + }); + + it('supports Substr', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('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').substring(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); + + it('supports Substr without length', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('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').substring(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + }); + + it('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 + ] + }); + }); + + it('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" + }); + }); + + it('testToUppercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(toUpper('author').as('uppercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + }); + + it('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" + }); + }); + + it('test reverse', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('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').equal(lastDoc.get('rating')), + field('__path__').greaterThan(lastDoc.ref?.id) + ), + field('rating').lessThan(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/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts new file mode 100644 index 00000000000..f11db5a427e --- /dev/null +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -0,0 +1,202 @@ +/** + * @license + * Copyright 2025 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 } from 'chai'; +import * as sinon from 'sinon'; + +import { Timestamp } from '../../../src'; +import { Firestore } from '../../../src/api/database'; +import { execute } from '../../../src/api/pipeline_impl'; +import { + MemoryOfflineComponentProvider, + OnlineComponentProvider +} from '../../../src/core/component_provider'; +import { + ExecutePipelineRequest as ProtoExecutePipelineRequest, + ExecutePipelineResponse as ProtoExecutePipelineResponse +} from '../../../src/protos/firestore_proto_api'; +import { newTestFirestore } from '../../util/api_helpers'; + +const FIRST_CALL = 0; +const EXECUTE_PIPELINE_REQUEST = 3; + +function fakePipelineResponse( + firestore: Firestore, + response?: ProtoExecutePipelineResponse[] +): sinon.SinonSpy { + response = response ?? [ + { + executionTime: Timestamp.now().toDate().toISOString(), + results: [] + } + ]; + const fake = sinon.fake.resolves(response); + + firestore._componentsProvider = { + _offline: { + build: () => new MemoryOfflineComponentProvider() + }, + _online: { + build: () => { + const provider = new OnlineComponentProvider(); + const ogCreateDatastore = provider.createDatastore.bind(provider); + provider.createDatastore = config => { + const datastore = ogCreateDatastore(config); + // @ts-ignore + datastore.invokeStreamingRPC = fake; + return datastore; + }; + return provider; + } + } + }; + + return fake; +} + +describe('execute(Pipeline|PipelineOptions)', () => { + it('returns execution time with empty results', async () => { + const firestore = newTestFirestore(); + + const executeTime = Timestamp.now(); + const spy = fakePipelineResponse(firestore, [ + { + executionTime: executeTime.toDate().toISOString(), + results: [] + } + ]); + + const pipelineSnapshot = await execute( + firestore.pipeline().collection('foo') + ); + + expect(pipelineSnapshot.results.length).to.equal(0); + expect(spy.calledOnce); + + expect(pipelineSnapshot.executionTime.toJSON()).to.deep.equal( + executeTime.toJSON() + ); + }); + + it('serializes the pipeline', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo') + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': {}, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + indexMode: 'recommended' + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'index_mode': { + 'stringValue': 'recommended' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline raw options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + rawOptions: { + 'foo': 'bar' + } + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'foo': { + 'stringValue': 'bar' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection', + 'options': {} + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); +}); diff --git a/packages/firestore/test/unit/core/options_util.test.ts b/packages/firestore/test/unit/core/options_util.test.ts new file mode 100644 index 00000000000..0549c2c8a9c --- /dev/null +++ b/packages/firestore/test/unit/core/options_util.test.ts @@ -0,0 +1,224 @@ +/** + * @license + * Copyright 2025 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 } from 'chai'; + +import { ParseContext } from '../../../src/api/parse_context'; +import { OptionsUtil } from '../../../src/core/options_util'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { testUserDataReader } from '../../util/helpers'; + +describe('OptionsUtil', () => { + let context: ParseContext | undefined; + beforeEach(async () => { + context = testUserDataReader(false).createContext( + UserDataSource.Argument, + 'beforeEach' + ); + }); + + afterEach(async () => { + context = undefined; + }); + + it('should support known options', () => { + const optionsUtil = new OptionsUtil({ + fooBar: { + serverName: 'foo_bar' + } + }); + const proto = optionsUtil.getOptionsProto(context!, { + fooBar: 'recommended' + }); + + expect(proto).deep.equal({ + 'foo_bar': { + stringValue: 'recommended' + } + }); + }); + + it('should support unknown options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto(context!, {}, { baz: 'foo' }); + + expect(proto).to.deep.equal({ + baz: { + stringValue: 'foo' + } + }); + }); + + it('should support unknown nested options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto( + context!, + {}, + { 'foo.bar': 'baz' } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { stringValue: 'baz' } + } + } + } + }); + }); + + it('should support options override', () => { + const optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode' + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + + expect(proto).to.deep.equal({ + 'index_mode': { + stringValue: 'baz' + } + }); + }); + + it('should support options override of nested field', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar' + }, + waldo: { + serverName: 'waldo' + } + } + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: { bar: 'yep', waldo: 'found' } + }, + { + 'foo.bar': 123, + 'foo.baz': true + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123' + }, + waldo: { + stringValue: 'found' + }, + baz: { + booleanValue: true + } + } + } + } + }); + }); + + it('will replace a nested object if given a new object', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar' + }, + waldo: { + serverName: 'waldo' + } + } + } + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: { bar: 'yep', waldo: 'found' } + }, + { + foo: { + bar: 123 + } + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123' + } + } + } + } + }); + }); + + it('will replace a top level property that is not an object if given a nested field with dot notation', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo' + } + }); + + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: 'bar' + }, + { + 'foo.bar': '123', + 'foo.waldo': true + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + stringValue: '123' + }, + waldo: { + booleanValue: true + } + } + } + } + }); + }); +}); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts new file mode 100644 index 00000000000..759dfecea44 --- /dev/null +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright 2025 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 } from 'chai'; +import * as sinon from 'sinon'; + +import { DatabaseId } from '../../../src/core/database_info'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../../../src/core/structured_pipeline'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; +import { + JsonProtoSerializer, + ProtoSerializable +} from '../../../src/remote/serializer'; +import { testUserDataReader } from '../../util/helpers'; + +describe('StructuredPipeline', () => { + it('should serialize the pipeline argument', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipelineOptions = new StructuredPipelineOptions(); + structuredPipelineOptions._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: {} + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support known options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + + const options = new StructuredPipelineOptions({ + indexMode: 'recommended' + }); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'index_mode': { + stringValue: 'recommended' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support unknown options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + {}, + { + 'foo_bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo_bar': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support unknown nested options', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + {}, + { + 'foo.bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: 'baz' } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support options override', () => { + const pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const options = new StructuredPipelineOptions( + { + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline(pipeline, options); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'index_mode': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); +}); 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": [ diff --git a/yarn.lock b/yarn.lock index 213ecd4d2c9..caa33e3d218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,11 +223,21 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@babel/helper-validator-option@^7.25.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" @@ -257,6 +267,13 @@ dependencies: "@babel/types" "^7.26.7" +"@babel/parser@^7.20.15": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== + dependencies: + "@babel/types" "^7.28.4" + "@babel/parser@^7.26.8": version "7.26.8" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz#deca2b4d99e5e1b1553843b99823f118da6107c2" @@ -1013,6 +1030,14 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@babel/types@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@bazel/runfiles@^6.3.1": version "6.3.1" resolved "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz#3f8824b2d82853377799d42354b4df78ab0ace0b" @@ -1588,6 +1613,13 @@ resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@jsdoc/salty@^0.2.1": + version "0.2.9" + resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" + integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== + dependencies: + lodash "^4.17.21" + "@kwsites/file-exists@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" @@ -3094,6 +3126,11 @@ dependencies: "@types/node" "*" +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + "@types/listr@0.14.9": version "0.14.9" resolved "https://registry.npmjs.org/@types/listr/-/listr-0.14.9.tgz#736581cfdfcdb821bace0a3e5b05e91182e00c85" @@ -3107,6 +3144,19 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/markdown-it@^14.1.1": + version "14.1.2" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/mime@^1": version "1.3.5" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -4617,7 +4667,7 @@ blocking-proxy@^1.0.0: dependencies: minimist "^1.2.0" -bluebird@3.7.2: +bluebird@3.7.2, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -5030,6 +5080,13 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + chai-as-promised@7.1.2: version "7.1.2" resolved "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz#70cd73b74afd519754161386421fb71832c6d041" @@ -6146,7 +6203,7 @@ deep-freeze@0.0.1: resolved "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" integrity sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -6613,6 +6670,11 @@ ent@~2.2.0: punycode "^1.4.1" safe-regex-test "^1.1.0" +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -6841,6 +6903,18 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escodegen@^1.13.0: + version "1.14.3" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + escodegen@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" @@ -6980,7 +7054,7 @@ esniff@^2.0.1: event-emitter "^0.3.5" type "^2.7.2" -espree@^9.6.0, espree@^9.6.1: +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -7008,7 +7082,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -7336,7 +7410,7 @@ fast-levenshtein@^1.0.0: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" integrity sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -8253,6 +8327,17 @@ glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.4.1: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -8414,7 +8499,7 @@ graceful-fs@4.2.10: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -10064,6 +10149,13 @@ js-yaml@~3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + jsbn@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -10074,6 +10166,27 @@ jsbn@~0.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== +jsdoc@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz#86565a9e39cc723a3640465b3fb189a22d1206ca" + integrity sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw== + dependencies: + "@babel/parser" "^7.20.15" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^14.1.1" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^14.1.0" + markdown-it-anchor "^8.6.7" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + underscore "~1.13.2" + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -10432,6 +10545,13 @@ klaw-sync@^6.0.0: dependencies: graceful-fs "^4.1.11" +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + kuler@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -10518,6 +10638,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + libnpmaccess@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz#dfb0e5b0a53c315a2610d300e46b4ddeb66e7eec" @@ -10577,6 +10705,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -11099,6 +11234,23 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it-anchor@^8.6.7: + version "8.6.7" + resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== + +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + marked-terminal@^7.0.0: version "7.2.1" resolved "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.2.1.tgz#9c1ae073a245a03c6a13e3eeac6f586f29856068" @@ -11122,6 +11274,11 @@ marked@^13.0.2: resolved "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d" integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== +marked@^4.0.10: + version "4.3.0" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + matchdep@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" @@ -11146,6 +11303,11 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -11345,7 +11507,7 @@ minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^5.1.0: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -12335,6 +12497,18 @@ optimist@~0.6.0: minimist "~0.0.1" wordwrap "~0.0.2" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -13126,6 +13300,11 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier@2.8.8, prettier@^2.7.1: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" @@ -13214,6 +13393,22 @@ proto3-json-serializer@^2.0.2: dependencies: protobufjs "^7.2.5" +protobufjs-cli@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz#c58b8566784f0fa1aff11e8d875a31de999637fe" + integrity sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ== + dependencies: + chalk "^4.0.0" + escodegen "^1.13.0" + espree "^9.0.0" + estraverse "^5.1.0" + glob "^8.0.0" + jsdoc "^4.0.0" + minimist "^1.2.0" + semver "^7.1.2" + tmp "^0.2.1" + uglify-js "^3.7.7" + protobufjs@7.4.0, protobufjs@^7.2.5, protobufjs@^7.3.2: version "7.4.0" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" @@ -13344,6 +13539,11 @@ pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -13909,6 +14109,13 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -14374,6 +14581,11 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semve resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.1.2: + version "7.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + semver@~7.3.0: version "7.3.8" resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" @@ -15298,7 +15510,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@3.1.1, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -15935,6 +16147,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -16111,7 +16330,12 @@ ua-parser-js@^0.7.30: resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562" integrity sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ== -uglify-js@^3.1.4, uglify-js@^3.4.9: +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + +uglify-js@^3.1.4, uglify-js@^3.4.9, uglify-js@^3.7.7: version "3.19.3" resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== @@ -16141,7 +16365,7 @@ unc-path-regex@^0.1.2: resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== -underscore@>=1.8.3, underscore@^1.9.1: +underscore@>=1.8.3, underscore@^1.9.1, underscore@~1.13.2: version "1.13.7" resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== @@ -16909,7 +17133,7 @@ winston@^3.0.0: triple-beam "^1.3.0" winston-transport "^4.9.0" -word-wrap@^1.2.5: +word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== @@ -17088,6 +17312,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"