diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index d3e31f5e52b..b8fca507e59 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -50,11 +50,14 @@ export type { export { PipelineSource } from '../../src/lite-api/pipeline-source'; -export { PipelineResult } from '../../src/lite-api/pipeline-result'; +export { + PipelineResult, + PipelineSnapshot +} from '../../src/lite-api/pipeline-result'; export { Pipeline } from '../../src/lite-api/pipeline'; -export { pipeline, execute } from '../../src/lite-api/pipeline_impl'; +export { execute } from '../../src/lite-api/pipeline_impl'; export { Stage, @@ -76,6 +79,8 @@ export { } from '../../src/lite-api/stage'; export { + field, + constant, add, subtract, multiply, @@ -132,79 +137,16 @@ export { timestampToUnixSeconds, timestampAdd, timestampSub, - genericFunction, ascending, descending, ExprWithAlias, Field, - Fields, Constant, - FirestoreFunction, - Add, - Subtract, - Multiply, - Divide, - Mod, - Eq, - Neq, - Lt, - Lte, - Gt, - Gte, - ArrayConcat, - ArrayReverse, - ArrayContains, - ArrayContainsAll, - ArrayContainsAny, - ArrayLength, - ArrayElement, - EqAny, - IsNan, - Exists, - Not, - And, - Or, - Xor, - Cond, - LogicalMaximum, - LogicalMinimum, - Reverse, - ReplaceFirst, - ReplaceAll, - CharLength, - ByteLength, - Like, - RegexContains, - RegexMatch, - StrContains, - StartsWith, - EndsWith, - ToLower, - ToUpper, - Trim, - StrConcat, - MapGet, - Count, - Sum, - Avg, - Minimum, - Maximum, - CosineDistance, - DotProduct, - EuclideanDistance, - VectorLength, - UnixMicrosToTimestamp, - TimestampToUnixMicros, - UnixMillisToTimestamp, - TimestampToUnixMillis, - UnixSecondsToTimestamp, - TimestampToUnixSeconds, - TimestampAdd, - TimestampSub, + FunctionExpr, Ordering, ExprType, - AccumulatorTarget, + AggregateWithAlias, Selectable, - FilterCondition, - Accumulator + BooleanExpr, + AggregateFunction } from '../../src/lite-api/expressions'; diff --git a/packages/firestore/src/api/pipeline.ts b/packages/firestore/src/api/pipeline.ts index aaddf00274b..e7cd6e875eb 100644 --- a/packages/firestore/src/api/pipeline.ts +++ b/packages/firestore/src/api/pipeline.ts @@ -15,16 +15,12 @@ * limitations under the License. */ -import { firestoreClientExecutePipeline } from '../core/firestore_client'; import { Pipeline as LitePipeline } from '../lite-api/pipeline'; -import { PipelineResult } from '../lite-api/pipeline-result'; -import { DocumentReference } from '../lite-api/reference'; import { Stage } from '../lite-api/stage'; import { UserDataReader } from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; -import { cast } from '../util/input_validation'; -import { ensureFirestoreConfigured, Firestore } from './database'; +import { Firestore } from './database'; export class Pipeline extends LitePipeline { /** @@ -41,66 +37,8 @@ export class Pipeline extends LitePipeline { db: Firestore, userDataReader: UserDataReader, userDataWriter: AbstractUserDataWriter, - stages: Stage[], - converter: unknown = {} + stages: Stage[] ): Pipeline { return new Pipeline(db, userDataReader, userDataWriter, stages); } - - /** - * 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 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: - * - *

- * - *

Example: - * - * ```typescript - * const futureResults = await firestore.pipeline().collection("books") - * .where(gt(Field.of("rating"), 4.5)) - * .select("title", "author", "rating") - * .execute(); - * ``` - * - * @return A Promise representing the asynchronous pipeline execution. - */ - execute(): Promise { - const firestore = cast(this._db, Firestore); - const client = ensureFirestoreConfigured(firestore); - return firestoreClientExecutePipeline(client, this).then(result => { - 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( - this._userDataWriter, - element.key?.path - ? new DocumentReference(firestore, null, element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - ) - ); - - return docs; - }); - } } diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index 9e7c25e69ab..3fbfbf5f7a6 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -16,16 +16,16 @@ */ import { Pipeline } from '../api/pipeline'; -import { toPipeline } from '../core/pipeline-util'; +import { firestoreClientExecutePipeline } from '../core/firestore_client'; import { Pipeline as LitePipeline } from '../lite-api/pipeline'; -import { PipelineResult } from '../lite-api/pipeline-result'; +import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; import { Stage } from '../lite-api/stage'; import { newUserDataReader } from '../lite-api/user_data_reader'; import { cast } from '../util/input_validation'; -import { Firestore } from './database'; -import { Query } from './reference'; +import { ensureFirestoreConfigured, Firestore } from './database'; +import { DocumentReference } from './reference'; import { ExpUserDataWriter } from './user_data_writer'; declare module './database' { @@ -35,49 +35,75 @@ declare module './database' { } /** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - -/** - * Experimental Modular API for console testing. - * @param query + * 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 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: + * + *

+ * + *

Example: + * + * ```typescript + * const futureResults = await execute(firestore.pipeline().collection("books") + * .where(gt(field("rating"), 4.5)) + * .select("title", "author", "rating")); + * ``` + * + * @param pipeline The pipeline to execute. + * @return A Promise representing the asynchronous pipeline execution. */ -export function pipeline(query: Query): Pipeline; +export function execute(pipeline: LitePipeline): Promise { + const firestore = cast(pipeline._db, Firestore); + const client = ensureFirestoreConfigured(firestore); + return firestoreClientExecutePipeline(client, pipeline).then(result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - if (firestoreOrQuery instanceof Firestore) { - const firestore = firestoreOrQuery; - return new PipelineSource((stages: Stage[]) => { - return new Pipeline( - firestore, - newUserDataReader(firestore), - new ExpUserDataWriter(firestore), - stages + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.key?.path + ? new DocumentReference(firestore, null, element.key) + : undefined, + element.fields, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) ); - }); - } else { - const query = firestoreOrQuery; - const db = cast(query.firestore, Firestore); - const litePipeline: LitePipeline = toPipeline(query._query, db); - return cast(litePipeline, Pipeline); - } -} - -export function execute(pipeline: LitePipeline): Promise { - return pipeline.execute(); + return new PipelineSnapshot(pipeline, docs, executionTime); + }); } // Augment the Firestore class with the pipeline() factory method Firestore.prototype.pipeline = function (): PipelineSource { - return pipeline(this); -}; - -// Augment the Query class with the pipeline() factory method -Query.prototype.pipeline = function (): Pipeline { - return pipeline(this); + return new PipelineSource(this._databaseId, (stages: Stage[]) => { + return new Pipeline( + this, + newUserDataReader(this), + new ExpUserDataWriter(this), + stages + ); + }); }; diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index 90fe836932f..1d5f98c54ab 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -17,11 +17,11 @@ export { PipelineSource } from './lite-api/pipeline-source'; -export { PipelineResult } from './lite-api/pipeline-result'; +export { PipelineResult, PipelineSnapshot } from './lite-api/pipeline-result'; export { Pipeline } from './api/pipeline'; -export { pipeline, execute } from './api/pipeline_impl'; +export { execute } from './api/pipeline_impl'; export { Stage, @@ -43,6 +43,8 @@ export { } from './lite-api/stage'; export { + field, + constant, add, subtract, multiply, @@ -104,84 +106,43 @@ export { timestampToUnixSeconds, timestampAdd, timestampSub, - genericFunction, ascending, descending, + countIf, + bitAnd, + bitOr, + bitXor, + bitNot, + bitLeftShift, + bitRightShift, + rand, + array, + arrayOffset, + currentContext, + isError, + ifError, + isAbsent, + isNull, + isNotNull, + isNotNan, + map, + mapRemove, + mapMerge, + documentIdFunction, + substr, + manhattanDistance, Expr, ExprWithAlias, Field, - Fields, Constant, - FirestoreFunction, - Add, - Subtract, - Multiply, - Divide, - Mod, - Eq, - Neq, - Lt, - Lte, - Gt, - Gte, - ArrayConcat, - ArrayReverse, - ArrayContains, - ArrayContainsAll, - ArrayContainsAny, - ArrayLength, - ArrayElement, - EqAny, - NotEqAny, - IsNan, - Exists, - Not, - And, - Or, - Xor, - Cond, - LogicalMaximum, - LogicalMinimum, - Reverse, - ReplaceFirst, - ReplaceAll, - CharLength, - ByteLength, - Like, - RegexContains, - RegexMatch, - StrContains, - StartsWith, - EndsWith, - ToLower, - ToUpper, - Trim, - StrConcat, - MapGet, - Count, - Sum, - Avg, - Minimum, - Maximum, - CosineDistance, - DotProduct, - EuclideanDistance, - VectorLength, - UnixMicrosToTimestamp, - TimestampToUnixMicros, - UnixMillisToTimestamp, - TimestampToUnixMillis, - UnixSecondsToTimestamp, - TimestampToUnixSeconds, - TimestampAdd, - TimestampSub, + FunctionExpr, Ordering } from './lite-api/expressions'; export type { ExprType, - AccumulatorTarget, + AggregateWithAlias, Selectable, - FilterCondition, - Accumulator + BooleanExpr, + AggregateFunction } from './lite-api/expressions'; diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts index 0800eba85ea..0a4f5d9e0fa 100644 --- a/packages/firestore/src/core/pipeline-util.ts +++ b/packages/firestore/src/core/pipeline-util.ts @@ -15,22 +15,20 @@ * limitations under the License. */ -import { Firestore } from '../api/database'; +import { Firestore } from '../lite-api/database'; import { Constant, Field, - FilterCondition, - not, + BooleanExpr, andFunction, orFunction, Ordering, - And, lt, gt, lte, gte, eq, - Or + field } from '../lite-api/expressions'; import { Pipeline } from '../lite-api/pipeline'; import { doc } from '../lite-api/reference'; @@ -56,56 +54,92 @@ import { /* eslint @typescript-eslint/no-explicit-any: 0 */ -export function toPipelineFilterCondition(f: FilterInternal): FilterCondition { +export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { if (f instanceof FieldFilterInternal) { - const field = Field.of(f.field.toString()); + const fieldValue = field(f.field.toString()); if (isNanValue(f.value)) { if (f.op === Operator.EQUAL) { - return andFunction(field.exists(), field.isNaN()); + return andFunction(fieldValue.exists(), fieldValue.isNan()); } else { - return andFunction(field.exists(), not(field.isNaN())); + return andFunction(fieldValue.exists(), fieldValue.isNotNan()); } } else if (isNullValue(f.value)) { if (f.op === Operator.EQUAL) { - return andFunction(field.exists(), field.eq(null)); + return andFunction(fieldValue.exists(), fieldValue.eq(null)); } else { - return andFunction(field.exists(), not(field.eq(null))); + return andFunction(fieldValue.exists(), fieldValue.neq(null)); } } else { // Comparison filters const value = f.value; switch (f.op) { case Operator.LESS_THAN: - return andFunction(field.exists(), field.lt(value)); + return andFunction( + fieldValue.exists(), + fieldValue.lt(Constant._fromProto(value)) + ); case Operator.LESS_THAN_OR_EQUAL: - return andFunction(field.exists(), field.lte(value)); + return andFunction( + fieldValue.exists(), + fieldValue.lte(Constant._fromProto(value)) + ); case Operator.GREATER_THAN: - return andFunction(field.exists(), field.gt(value)); + return andFunction( + fieldValue.exists(), + fieldValue.gt(Constant._fromProto(value)) + ); case Operator.GREATER_THAN_OR_EQUAL: - return andFunction(field.exists(), field.gte(value)); + return andFunction( + fieldValue.exists(), + fieldValue.gte(Constant._fromProto(value)) + ); case Operator.EQUAL: - return andFunction(field.exists(), field.eq(value)); + return andFunction( + fieldValue.exists(), + fieldValue.eq(Constant._fromProto(value)) + ); case Operator.NOT_EQUAL: - return andFunction(field.exists(), field.neq(value)); + return andFunction( + fieldValue.exists(), + fieldValue.neq(Constant._fromProto(value)) + ); case Operator.ARRAY_CONTAINS: - return andFunction(field.exists(), field.arrayContains(value)); + return andFunction( + fieldValue.exists(), + fieldValue.arrayContains(Constant._fromProto(value)) + ); case Operator.IN: { const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) + Constant._fromProto(val) ); - return andFunction(field.exists(), field.eqAny(...values!)); + if (!values) { + return fieldValue.exists(); + } else if (values.length == 1) { + return andFunction(fieldValue.exists(), fieldValue.eq(values[0])); + } else { + return andFunction(fieldValue.exists(), fieldValue.eqAny(values)); + } } case Operator.ARRAY_CONTAINS_ANY: { const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) + Constant._fromProto(val) + ); + return andFunction( + fieldValue.exists(), + fieldValue.arrayContainsAny(values!) ); - return andFunction(field.exists(), field.arrayContainsAny(values!)); } case Operator.NOT_IN: { const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) + Constant._fromProto(val) ); - return andFunction(field.exists(), not(field.eqAny(...values!))); + if (!values) { + return fieldValue.exists(); + } else if (values.length == 1) { + return andFunction(fieldValue.exists(), fieldValue.neq(values[0])); + } else { + return andFunction(fieldValue.exists(), fieldValue.notEqAny(values)); + } } default: fail('Unexpected operator'); @@ -114,16 +148,16 @@ export function toPipelineFilterCondition(f: FilterInternal): FilterCondition { } else if (f instanceof CompositeFilterInternal) { switch (f.op) { case CompositeOperator.AND: { - const conditions = f - .getFilters() - .map(f => toPipelineFilterCondition(f)); - return andFunction(conditions[0], ...conditions.slice(1)); + const conditions = f.getFilters().map(f => toPipelineBooleanExpr(f)); + return andFunction( + conditions[0], + conditions[1], + ...conditions.slice(2) + ); } case CompositeOperator.OR: { - const conditions = f - .getFilters() - .map(f => toPipelineFilterCondition(f)); - return orFunction(conditions[0], ...conditions.slice(1)); + const conditions = f.getFilters().map(f => toPipelineBooleanExpr(f)); + return orFunction(conditions[0], conditions[1], ...conditions.slice(2)); } default: fail('Unexpected operator'); @@ -155,17 +189,21 @@ export function toPipeline(query: Query, db: Firestore): Pipeline { // filters for (const filter of query.filters) { - pipeline = pipeline.where(toPipelineFilterCondition(filter)); + pipeline = pipeline.where(toPipelineBooleanExpr(filter)); } // orders const orders = queryNormalizedOrderBy(query); const existsConditions = orders.map(order => - Field.of(order.field.canonicalString()).exists() + field(order.field.canonicalString()).exists() ); if (existsConditions.length > 1) { pipeline = pipeline.where( - andFunction(existsConditions[0], ...existsConditions.slice(1)) + andFunction( + existsConditions[0], + existsConditions[1], + ...existsConditions.slice(2) + ) ); } else { pipeline = pipeline.where(existsConditions[0]); @@ -173,42 +211,45 @@ export function toPipeline(query: Query, db: Firestore): Pipeline { const orderings = orders.map(order => order.dir === Direction.ASCENDING - ? Field.of(order.field.canonicalString()).ascending() - : Field.of(order.field.canonicalString()).descending() + ? field(order.field.canonicalString()).ascending() + : field(order.field.canonicalString()).descending() ); - if (query.limitType === LimitType.Last) { - pipeline = pipeline.sort(...reverseOrderings(orderings)); - // cursors - if (query.startAt !== null) { - pipeline = pipeline.where( - whereConditionsFromCursor(query.startAt, orderings, 'before') - ); - } + 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, 'before') + ); + } - if (query.endAt !== null) { - pipeline = pipeline.where( - whereConditionsFromCursor(query.endAt, orderings, 'after') - ); - } + if (query.endAt !== null) { + pipeline = pipeline.where( + whereConditionsFromCursor(query.endAt, orderings, 'after') + ); + } - pipeline = pipeline._limit(query.limit!, true); - pipeline = pipeline.sort(...orderings); - } else { - pipeline = pipeline.sort(...orderings); - 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!, true); + 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); + if (query.limit !== null) { + pipeline = pipeline.limit(query.limit); + } } } @@ -219,19 +260,19 @@ function whereConditionsFromCursor( bound: Bound, orderings: Ordering[], position: 'before' | 'after' -): FilterCondition { +): BooleanExpr { const cursors = bound.position.map(value => Constant._fromProto(value)); const filterFunc = position === 'before' ? lt : gt; const filterInclusiveFunc = position === 'before' ? lte : gte; - const orConditions = []; + const orConditions: BooleanExpr[] = []; for (let i = 1; i <= orderings.length; i++) { const cursorSubset = cursors.slice(0, i); - const conditions = cursorSubset.map((cursor, index) => { + const conditions: BooleanExpr[] = cursorSubset.map((cursor, index) => { if (index < cursorSubset.length - 1) { return eq(orderings[index].expr as Field, cursor); - } else if (!!bound.inclusive && i === orderings.length) { + } else if (bound.inclusive && i === orderings.length) { return filterInclusiveFunc(orderings[index].expr as Field, cursor); } else { return filterFunc(orderings[index].expr as Field, cursor); @@ -241,13 +282,19 @@ function whereConditionsFromCursor( if (conditions.length === 1) { orConditions.push(conditions[0]); } else { - orConditions.push(new And(conditions)); + orConditions.push( + andFunction(conditions[0], conditions[1], ...conditions.slice(2)) + ); } } if (orConditions.length === 1) { return orConditions[0]; } else { - return new Or(orConditions); + return orFunction( + orConditions[0], + orConditions[1], + ...orConditions.slice(2) + ); } } diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 03e5c5e747e..651a682eb96 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -25,16 +25,19 @@ import { Value as ProtoValue } from '../protos/firestore_proto_api'; import { JsonProtoSerializer, ProtoSerializable, + ProtoValueSerializable, + toMapValue, toStringValue, UserData } from '../remote/serializer'; import { hardAssert } from '../util/assert'; +import { isPlainObject } from '../util/input_validation'; import { isFirestoreValue } from '../util/proto'; +import { isString } from '../util/types'; import { Bytes } from './bytes'; import { documentId, FieldPath } from './field_path'; import { GeoPoint } from './geo_point'; -import { Pipeline } from './pipeline'; import { DocumentReference } from './reference'; import { Timestamp } from './timestamp'; import { @@ -54,9 +57,64 @@ export type ExprType = | 'Field' | 'Constant' | 'Function' + | 'AggregateFunction' | 'ListOfExprs' | 'ExprWithAlias'; +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +function valueToDefaultExpr(value: any): Expr { + if (value instanceof Expr) { + return value; + } else if (isPlainObject(value)) { + return map(value); + } else if (value instanceof Array) { + return array(value); + } else { + return constant(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 + */ +function vectorToExpr(value: VectorValue | number[] | Expr): Expr { + if (value instanceof Expr) { + return value; + } else { + return constantVector(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 fieldOfOrExpr(value: any): Expr { + if (isString(value)) { + return field(value); + } else { + return valueToDefaultExpr(value); + } +} + /** * @beta * @@ -69,44 +127,44 @@ export type ExprType = * - **Field references:** Access values from document fields. * - **Literals:** Represent constant values (strings, numbers, booleans). * - **Function calls:** Apply functions to one or more expressions. - * - **Aggregations:** Calculate aggregate values (e.g., sum, average) over a set of documents. * * The `Expr` class provides a fluent API for building expressions. You can chain together * method calls to create complex expressions. */ -export abstract class Expr implements ProtoSerializable, UserData { - abstract exprType: ExprType; +export abstract class Expr implements ProtoValueSerializable, UserData { + abstract readonly exprType: ExprType; /** - * Creates an expression that adds this expression to another expression. - * - * ```typescript - * // Add the value of the 'quantity' field and the 'reserve' field. - * Field.of("quantity").add(Field.of("reserve")); - * ``` - * - * @param other The expression to add to this expression. - * @return A new `Expr` representing the addition operation. + * @private + * @internal + */ + abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; + _protoValueType = 'ProtoValue' as const; + + /** + * @private + * @internal */ - add(other: Expr): Add; + abstract _readUserData(dataReader: UserDataReader): void; /** - * Creates an expression that adds this expression to a constant value. + * Creates an expression that adds this expression to another expression. * * ```typescript - * // Add 5 to the value of the 'age' field - * Field.of("age").add(5); + * // Add the value of the 'quantity' field and the 'reserve' field. + * field("quantity").add(field("reserve")); * ``` * - * @param other The constant value to add. + * @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(other: any): Add; - add(other: any): Add { - if (other instanceof Expr) { - return new Add(this, other); - } - return new Add(this, Constant.of(other)); + add(second: Expr | any, ...others: Array): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('add', [ + this, + ...values.map(value => valueToDefaultExpr(value)) + ]); } /** @@ -114,31 +172,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Subtract the 'discount' field from the 'price' field - * Field.of("price").subtract(Field.of("discount")); + * field("price").subtract(field("discount")); * ``` * * @param other The expression to subtract from this expression. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: Expr): Subtract; + subtract(other: Expr): FunctionExpr; /** * Creates an expression that subtracts a constant value from this expression. * * ```typescript * // Subtract 20 from the value of the 'total' field - * Field.of("total").subtract(20); + * field("total").subtract(20); * ``` * * @param other The constant value to subtract. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: any): Subtract; - subtract(other: any): Subtract { - if (other instanceof Expr) { - return new Subtract(this, other); - } - return new Subtract(this, Constant.of(other)); + subtract(other: any): FunctionExpr; + subtract(other: any): FunctionExpr { + return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)]); } /** @@ -146,31 +201,19 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Multiply the 'quantity' field by the 'price' field - * Field.of("quantity").multiply(Field.of("price")); - * ``` - * - * @param other The expression to multiply by. - * @return A new `Expr` representing the multiplication operation. - */ - multiply(other: Expr): Multiply; - - /** - * Creates an expression that multiplies this expression by a constant value. - * - * ```typescript - * // Multiply the 'value' field by 2 - * Field.of("value").multiply(2); + * field("quantity").multiply(field("price")); * ``` * - * @param other The constant value to multiply by. + * @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(other: any): Multiply; - multiply(other: any): Multiply { - if (other instanceof Expr) { - return new Multiply(this, other); - } - return new Multiply(this, Constant.of(other)); + multiply(second: Expr | any, ...others: Array): FunctionExpr { + return new FunctionExpr('multiply', [ + this, + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ]); } /** @@ -178,31 +221,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Divide the 'total' field by the 'count' field - * Field.of("total").divide(Field.of("count")); + * field("total").divide(field("count")); * ``` * * @param other The expression to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: Expr): Divide; + divide(other: Expr): FunctionExpr; /** * Creates an expression that divides this expression by a constant value. * * ```typescript * // Divide the 'value' field by 10 - * Field.of("value").divide(10); + * field("value").divide(10); * ``` * * @param other The constant value to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: any): Divide; - divide(other: any): Divide { - if (other instanceof Expr) { - return new Divide(this, other); - } - return new Divide(this, Constant.of(other)); + divide(other: any): FunctionExpr; + divide(other: any): FunctionExpr { + return new FunctionExpr('divide', [this, valueToDefaultExpr(other)]); } /** @@ -210,237 +250,57 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the remainder of dividing the 'value' field by the 'divisor' field - * Field.of("value").mod(Field.of("divisor")); + * field("value").mod(field("divisor")); * ``` * - * @param other The expression to divide by. + * @param expression The expression to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(other: Expr): Mod; + mod(expression: Expr): FunctionExpr; /** * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. * * ```typescript * // Calculate the remainder of dividing the 'value' field by 10 - * Field.of("value").mod(10); + * field("value").mod(10); * ``` * - * @param other The constant value to divide by. + * @param value The constant value to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(other: any): Mod; - mod(other: any): Mod { - if (other instanceof Expr) { - return new Mod(this, other); - } - return new Mod(this, Constant.of(other)); + mod(value: any): FunctionExpr; + mod(other: any): FunctionExpr { + return new FunctionExpr('mod', [this, valueToDefaultExpr(other)]); } - // /** - // * Creates an expression that applies a bitwise AND operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise AND of 'field1' and 'field2'. - // * Field.of("field1").bitAnd(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise AND operation. - // */ - // bitAnd(other: Expr): BitAnd; - // - // /** - // * Creates an expression that applies a bitwise AND operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise AND of 'field1' and 0xFF. - // * Field.of("field1").bitAnd(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise AND operation. - // */ - // bitAnd(other: any): BitAnd; - // bitAnd(other: any): BitAnd { - // if (other instanceof Expr) { - // return new BitAnd(this, other); - // } - // return new BitAnd(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise OR operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise OR of 'field1' and 'field2'. - // * Field.of("field1").bitOr(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise OR operation. - // */ - // bitOr(other: Expr): BitOr; - // - // /** - // * Creates an expression that applies a bitwise OR operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise OR of 'field1' and 0xFF. - // * Field.of("field1").bitOr(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise OR operation. - // */ - // bitOr(other: any): BitOr; - // bitOr(other: any): BitOr { - // if (other instanceof Expr) { - // return new BitOr(this, other); - // } - // return new BitOr(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise XOR operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise XOR of 'field1' and 'field2'. - // * Field.of("field1").bitXor(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise XOR operation. - // */ - // bitXor(other: Expr): BitXor; - // - // /** - // * Creates an expression that applies a bitwise XOR operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise XOR of 'field1' and 0xFF. - // * Field.of("field1").bitXor(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise XOR operation. - // */ - // bitXor(other: any): BitXor; - // bitXor(other: any): BitXor { - // if (other instanceof Expr) { - // return new BitXor(this, other); - // } - // return new BitXor(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise NOT operation to this expression. - // * - // * ```typescript - // * // Calculate the bitwise NOT of 'field1'. - // * Field.of("field1").bitNot(); - // * ``` - // * - // * @return A new {@code Expr} representing the bitwise NOT operation. - // */ - // bitNot(): BitNot { - // return new BitNot(this); - // } - // - // /** - // * Creates an expression that applies a bitwise left shift operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - // * Field.of("field1").bitLeftShift(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise left shift operation. - // */ - // bitLeftShift(other: Expr): BitLeftShift; - // - // /** - // * Creates an expression that applies a bitwise left shift operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise left shift of 'field1' by 2 bits. - // * Field.of("field1").bitLeftShift(2); - // * ``` - // * - // * @param other The right operand constant representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise left shift operation. - // */ - // bitLeftShift(other: number): BitLeftShift; - // bitLeftShift(other: Expr | number): BitLeftShift { - // if (typeof other === 'number') { - // return new BitLeftShift(this, Constant.of(other)); - // } - // return new BitLeftShift(this, other as Expr); - // } - // - // /** - // * Creates an expression that applies a bitwise right shift operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - // * Field.of("field1").bitRightShift(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise right shift operation. - // */ - // bitRightShift(other: Expr): BitRightShift; - // - // /** - // * Creates an expression that applies a bitwise right shift operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise right shift of 'field1' by 2 bits. - // * Field.of("field1").bitRightShift(2); - // * ``` - // * - // * @param other The right operand constant representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise right shift operation. - // */ - // bitRightShift(other: number): BitRightShift; - // bitRightShift(other: Expr | number): BitRightShift { - // if (typeof other === 'number') { - // return new BitRightShift(this, Constant.of(other)); - // } - // return new BitRightShift(this, other as Expr); - // } - /** * Creates an expression that checks if this expression is equal to another expression. * * ```typescript * // Check if the 'age' field is equal to 21 - * Field.of("age").eq(21); + * field("age").eq(21); * ``` * - * @param other The expression to compare for equality. + * @param expression The expression to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(other: Expr): Eq; + eq(expression: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is equal to a constant value. * * ```typescript * // Check if the 'city' field is equal to "London" - * Field.of("city").eq("London"); + * field("city").eq("London"); * ``` * - * @param other The constant value to compare for equality. + * @param value The constant value to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(other: any): Eq; - eq(other: any): Eq { - if (other instanceof Expr) { - return new Eq(this, other); - } - return new Eq(this, Constant.of(other)); + eq(value: any): BooleanExpr; + eq(other: any): BooleanExpr { + return new BooleanExpr('eq', [this, valueToDefaultExpr(other)]); } /** @@ -448,31 +308,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'status' field is not equal to "completed" - * Field.of("status").neq("completed"); + * field("status").neq("completed"); * ``` * - * @param other The expression to compare for inequality. + * @param expression The expression to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(other: Expr): Neq; + neq(expression: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is not equal to a constant value. * * ```typescript * // Check if the 'country' field is not equal to "USA" - * Field.of("country").neq("USA"); + * field("country").neq("USA"); * ``` * - * @param other The constant value to compare for inequality. + * @param value The constant value to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(other: any): Neq; - neq(other: any): Neq { - if (other instanceof Expr) { - return new Neq(this, other); - } - return new Neq(this, Constant.of(other)); + neq(value: any): BooleanExpr; + neq(other: any): BooleanExpr { + return new BooleanExpr('neq', [this, valueToDefaultExpr(other)]); } /** @@ -480,31 +337,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'age' field is less than 'limit' - * Field.of("age").lt(Field.of('limit')); + * field("age").lt(field('limit')); * ``` * - * @param other The expression to compare for less than. + * @param experession The expression to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(other: Expr): Lt; + lt(experession: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is less than a constant value. * * ```typescript * // Check if the 'price' field is less than 50 - * Field.of("price").lt(50); + * field("price").lt(50); * ``` * - * @param other The constant value to compare for less than. + * @param value The constant value to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(other: any): Lt; - lt(other: any): Lt { - if (other instanceof Expr) { - return new Lt(this, other); - } - return new Lt(this, Constant.of(other)); + lt(value: any): BooleanExpr; + lt(other: any): BooleanExpr { + return new BooleanExpr('lt', [this, valueToDefaultExpr(other)]); } /** @@ -513,31 +367,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * Field.of("quantity").lte(Constant.of(20)); + * field("quantity").lte(constant(20)); * ``` * - * @param other The expression to compare for less than or equal to. + * @param expression The expression to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(other: Expr): Lte; + lte(expression: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is less than or equal to a constant value. * * ```typescript * // Check if the 'score' field is less than or equal to 70 - * Field.of("score").lte(70); + * field("score").lte(70); * ``` * - * @param other The constant value to compare for less than or equal to. + * @param value The constant value to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(other: any): Lte; - lte(other: any): Lte { - if (other instanceof Expr) { - return new Lte(this, other); - } - return new Lte(this, Constant.of(other)); + lte(value: any): BooleanExpr; + lte(other: any): BooleanExpr { + return new BooleanExpr('lte', [this, valueToDefaultExpr(other)]); } /** @@ -545,31 +396,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'age' field is greater than the 'limit' field - * Field.of("age").gt(Field.of("limit")); + * field("age").gt(field("limit")); * ``` * - * @param other The expression to compare for greater than. + * @param expression The expression to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(other: Expr): Gt; + gt(expression: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is greater than a constant value. * * ```typescript * // Check if the 'price' field is greater than 100 - * Field.of("price").gt(100); + * field("price").gt(100); * ``` * - * @param other The constant value to compare for greater than. + * @param value The constant value to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(other: any): Gt; - gt(other: any): Gt { - if (other instanceof Expr) { - return new Gt(this, other); - } - return new Gt(this, Constant.of(other)); + gt(value: any): BooleanExpr; + gt(other: any): BooleanExpr { + return new BooleanExpr('gt', [this, valueToDefaultExpr(other)]); } /** @@ -578,13 +426,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is greater than or equal to field 'requirement' plus 1 - * Field.of("quantity").gte(Field.of('requirement').add(1)); + * field("quantity").gte(field('requirement').add(1)); * ``` * - * @param other The expression to compare for greater than or equal to. + * @param expression The expression to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(other: Expr): Gte; + gte(expression: Expr): BooleanExpr; /** * Creates an expression that checks if this expression is greater than or equal to a constant @@ -592,18 +440,15 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'score' field is greater than or equal to 80 - * Field.of("score").gte(80); + * field("score").gte(80); * ``` * - * @param other The constant value to compare for greater than or equal to. + * @param value The constant value to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(other: any): Gte; - gte(other: any): Gte { - if (other instanceof Expr) { - return new Gte(this, other); - } - return new Gte(this, Constant.of(other)); + gte(value: any): BooleanExpr; + gte(other: any): BooleanExpr { + return new BooleanExpr('gte', [this, valueToDefaultExpr(other)]); } /** @@ -611,31 +456,19 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Combine the 'items' array with another array field. - * Field.of("items").arrayConcat(Field.of("otherItems")); + * field("items").arrayConcat(field("otherItems")); * ``` - * - * @param arrays The array expressions to concatenate. + * @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(...arrays: Expr[]): ArrayConcat; - - /** - * Creates an expression that concatenates an array with one or more other arrays. - * - * ```typescript - * // Combine the 'tags' array with a new array and an array field - * Field.of("tags").arrayConcat(Arrays.asList("newTag1", "newTag2"), Field.of("otherTag")); - * ``` - * - * @param arrays The arrays to concatenate. - * @return A new `Expr` representing the concatenated arrays. - */ - arrayConcat(...arrays: any[][]): ArrayConcat; - arrayConcat(...arrays: any[]): ArrayConcat { - const exprValues = arrays.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayConcat(this, exprValues); + arrayConcat( + secondArray: Expr | any[], + ...otherArrays: Array + ): FunctionExpr { + const elements = [secondArray, ...otherArrays]; + const exprValues = elements.map(value => valueToDefaultExpr(value)); + return new FunctionExpr('array_concat', [this, ...exprValues]); } /** @@ -643,63 +476,63 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'sizes' array contains the value from the 'selectedSize' field - * Field.of("sizes").arrayContains(Field.of("selectedSize")); + * field("sizes").arrayContains(field("selectedSize")); * ``` * - * @param element The element to search for in the array. + * @param expression The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(element: Expr): ArrayContains; + arrayContains(expression: Expr): BooleanExpr; /** * Creates an expression that checks if an array contains a specific value. * * ```typescript * // Check if the 'colors' array contains "red" - * Field.of("colors").arrayContains("red"); + * field("colors").arrayContains("red"); * ``` * - * @param element The element to search for in the array. + * @param value The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(element: any): ArrayContains; - arrayContains(element: any): ArrayContains { - if (element instanceof Expr) { - return new ArrayContains(this, element); - } - return new ArrayContains(this, Constant.of(element)); + arrayContains(value: any): BooleanExpr; + arrayContains(element: any): BooleanExpr { + return new BooleanExpr('array_contains', [ + this, + valueToDefaultExpr(element) + ]); } /** * Creates an expression that checks if an array contains all the specified elements. * * ```typescript - * // Check if the 'tags' array contains both "news" and "sports" - * Field.of("tags").arrayContainsAll(Field.of("tag1"), Field.of("tag2")); + * // 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: Expr[]): ArrayContainsAll; + arrayContainsAll(values: Array): BooleanExpr; /** * Creates an expression that checks if an array contains all the specified elements. * * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * Field.of("tags").arrayContainsAll(Field.of("tag1"), Field.of("tag2")); + * // 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 values The elements to check for in the array. + * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_all' comparison. */ - arrayContainsAll(...values: any[]): ArrayContainsAll; - arrayContainsAll(...values: any[]): ArrayContainsAll { - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAll(this, exprValues); + arrayContainsAll(arrayExpression: Expr): BooleanExpr; + arrayContainsAll(values: any[] | Expr): BooleanExpr { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr)) + : values; + return new BooleanExpr('array_contains_all', [this, normalizedExpr]); } /** @@ -707,13 +540,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'categories' array contains either values from field "cate1" or "cate2" - * Field.of("categories").arrayContainsAny(Field.of("cate1"), Field.of("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: Expr[]): ArrayContainsAny; + arrayContainsAny(values: Array): BooleanExpr; /** * Creates an expression that checks if an array contains any of the specified elements. @@ -721,18 +554,18 @@ export abstract class Expr implements ProtoSerializable, UserData { * ```typescript * // Check if the 'groups' array contains either the value from the 'userGroup' field * // or the value "guest" - * Field.of("groups").arrayContainsAny(Field.of("userGroup"), "guest"); + * field("groups").arrayContainsAny(array([field("userGroup"), "guest"])); * ``` * - * @param values The elements to check for in the array. + * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_any' comparison. */ - arrayContainsAny(...values: any[]): ArrayContainsAny; - arrayContainsAny(...values: any[]): ArrayContainsAny { - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAny(this, exprValues); + arrayContainsAny(arrayExpression: Expr): BooleanExpr; + arrayContainsAny(values: Array | Expr): BooleanExpr { + const normalizedExpr = Array.isArray(values) + ? new ListOfExprs(values.map(valueToDefaultExpr)) + : values; + return new BooleanExpr('array_contains_any', [this, normalizedExpr]); } /** @@ -740,13 +573,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Get the number of items in the 'cart' array - * Field.of("cart").arrayLength(); + * field("cart").arrayLength(); * ``` * * @return A new `Expr` representing the length of the array. */ - arrayLength(): ArrayLength { - return new ArrayLength(this); + arrayLength(): FunctionExpr { + return new FunctionExpr('array_length', [this]); } /** @@ -755,13 +588,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * Field.of("category").eqAny("Electronics", Field.of("primaryType")); + * field("category").eqAny("Electronics", field("primaryType")); * ``` * - * @param others The values or expressions to check against. + * @param values The values or expressions to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(...others: Expr[]): EqAny; + eqAny(values: Array): BooleanExpr; /** * Creates an expression that checks if this expression is equal to any of the provided values or @@ -769,18 +602,18 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * Field.of("category").eqAny("Electronics", Field.of("primaryType")); + * field("category").eqAny(array(["Electronics", field("primaryType")])); * ``` * - * @param others The values or expressions to check against. + * @param arrayExpression An expression that evaluates to an array of values to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(...others: any[]): EqAny; - eqAny(...others: any[]): EqAny { - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new EqAny(this, exprOthers); + eqAny(arrayExpression: Expr): BooleanExpr; + eqAny(others: any[] | Expr): BooleanExpr { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr)) + : others; + return new BooleanExpr('eq_any', [this, exprOthers]); } /** @@ -789,32 +622,31 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * Field.of("status").notEqAny("pending", Field.of("rejectedStatus")); + * field("status").notEqAny([]"pending", field("rejectedStatus")]); * ``` * - * @param others The values or expressions to check against. + * @param values The values or expressions to check against. * @return A new `Expr` representing the 'NotEqAny' comparison. */ - notEqAny(...others: Expr[]): NotEqAny; + notEqAny(values: Array): BooleanExpr; /** - * Creates an expression that checks if this expression is not equal to any of the provided values or - * expressions. + * 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 neither "pending" nor the value of 'rejectedStatus' - * Field.of("status").notEqAny("pending", Field.of("rejectedStatus")); + * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' + * field("status").notEqAny(field('rejectedStatuses')); * ``` * - * @param others The values or expressions to check against. + * @param arrayExpression The values or expressions to check against. * @return A new `Expr` representing the 'NotEqAny' comparison. */ - notEqAny(...others: any[]): NotEqAny; - notEqAny(...others: any[]): NotEqAny { - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new NotEqAny(this, exprOthers); + notEqAny(arrayExpression: Expr): BooleanExpr; + notEqAny(others: any[] | Expr): BooleanExpr { + const exprOthers = Array.isArray(others) + ? new ListOfExprs(others.map(valueToDefaultExpr)) + : others; + return new BooleanExpr('not_eq_any', [this, exprOthers]); } /** @@ -822,13 +654,27 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the result of a calculation is NaN - * Field.of("value").divide(0).isNaN(); + * field("value").divide(0).isNaN(); * ``` * * @return A new `Expr` representing the 'isNaN' check. */ - isNaN(): IsNan { - return new IsNan(this); + isNan(): BooleanExpr { + return new BooleanExpr('is_nan', [this]); + } + + /** + * Creates an expression that checks if this expression evaluates to 'Null'. + * + * ```typescript + * // Check if the result of a calculation is NaN + * field("value").isNull(); + * ``` + * + * @return A new `Expr` representing the 'isNull' check. + */ + isNull(): BooleanExpr { + return new BooleanExpr('is_null', [this]); } /** @@ -836,13 +682,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the document has a field named "phoneNumber" - * Field.of("phoneNumber").exists(); + * field("phoneNumber").exists(); * ``` * * @return A new `Expr` representing the 'exists' check. */ - exists(): Exists { - return new Exists(this); + exists(): BooleanExpr { + return new BooleanExpr('exists', [this]); } /** @@ -850,13 +696,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Get the character length of the 'name' field in its UTF-8 form. - * Field.of("name").charLength(); + * field("name").charLength(); * ``` * * @return A new `Expr` representing the length of the string. */ - charLength(): CharLength { - return new CharLength(this); + charLength(): FunctionExpr { + return new FunctionExpr('char_length', [this]); } /** @@ -864,31 +710,28 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'title' field contains the word "guide" (case-sensitive) - * Field.of("title").like("%guide%"); + * 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): Like; + like(pattern: string): FunctionExpr; /** * Creates an expression that performs a case-sensitive string comparison. * * ```typescript * // Check if the 'title' field contains the word "guide" (case-sensitive) - * Field.of("title").like("%guide%"); + * field("title").like("%guide%"); * ``` * * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new `Expr` representing the 'like' comparison. */ - like(pattern: Expr): Like; - like(stringOrExpr: string | Expr): Like { - if (typeof stringOrExpr === 'string') { - return new Like(this, Constant.of(stringOrExpr)); - } - return new Like(this, stringOrExpr as Expr); + like(pattern: Expr): FunctionExpr; + like(stringOrExpr: string | Expr): FunctionExpr { + return new FunctionExpr('like', [this, valueToDefaultExpr(stringOrExpr)]); } /** @@ -897,13 +740,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'description' field contains "example" (case-insensitive) - * Field.of("description").regexContains("(?i)example"); + * 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): RegexContains; + regexContains(pattern: string): BooleanExpr; /** * Creates an expression that checks if a string contains a specified regular expression as a @@ -911,18 +754,18 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'description' field contains the regular expression stored in field 'regex' - * Field.of("description").regexContains(Field.of("regex")); + * field("description").regexContains(field("regex")); * ``` * * @param pattern The regular expression to use for the search. * @return A new `Expr` representing the 'contains' comparison. */ - regexContains(pattern: Expr): RegexContains; - regexContains(stringOrExpr: string | Expr): RegexContains { - if (typeof stringOrExpr === 'string') { - return new RegexContains(this, Constant.of(stringOrExpr)); - } - return new RegexContains(this, stringOrExpr as Expr); + regexContains(pattern: Expr): BooleanExpr; + regexContains(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('regex_contains', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); } /** @@ -930,31 +773,31 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'email' field matches a valid email pattern - * Field.of("email").regexMatch("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); + * 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): RegexMatch; + regexMatch(pattern: string): BooleanExpr; /** * Creates an expression that checks if a string matches a specified regular expression. * * ```typescript * // Check if the 'email' field matches a regular expression stored in field 'regex' - * Field.of("email").regexMatch(Field.of("regex")); + * field("email").regexMatch(field("regex")); * ``` * * @param pattern The regular expression to use for the match. * @return A new `Expr` representing the regular expression match. */ - regexMatch(pattern: Expr): RegexMatch; - regexMatch(stringOrExpr: string | Expr): RegexMatch { - if (typeof stringOrExpr === 'string') { - return new RegexMatch(this, Constant.of(stringOrExpr)); - } - return new RegexMatch(this, stringOrExpr as Expr); + regexMatch(pattern: Expr): BooleanExpr; + regexMatch(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('regex_match', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); } /** @@ -962,31 +805,31 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'description' field contains "example". - * Field.of("description").strContains("example"); + * field("description").strContains("example"); * ``` * * @param substring The substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(substring: string): StrContains; + strContains(substring: string): BooleanExpr; /** * Creates an expression that checks if a string contains the string represented by another expression. * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * Field.of("description").strContains(Field.of("keyword")); + * field("description").strContains(field("keyword")); * ``` * * @param expr The expression representing the substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(expr: Expr): StrContains; - strContains(stringOrExpr: string | Expr): StrContains { - if (typeof stringOrExpr === 'string') { - return new StrContains(this, Constant.of(stringOrExpr)); - } - return new StrContains(this, stringOrExpr as Expr); + strContains(expr: Expr): BooleanExpr; + strContains(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('str_contains', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); } /** @@ -994,13 +837,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'name' field starts with "Mr." - * Field.of("name").startsWith("Mr."); + * field("name").startsWith("Mr."); * ``` * * @param prefix The prefix to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: string): StartsWith; + startsWith(prefix: string): BooleanExpr; /** * Creates an expression that checks if a string starts with a given prefix (represented as an @@ -1008,18 +851,18 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'fullName' field starts with the value of the 'firstName' field - * Field.of("fullName").startsWith(Field.of("firstName")); + * field("fullName").startsWith(field("firstName")); * ``` * * @param prefix The prefix expression to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: Expr): StartsWith; - startsWith(stringOrExpr: string | Expr): StartsWith { - if (typeof stringOrExpr === 'string') { - return new StartsWith(this, Constant.of(stringOrExpr)); - } - return new StartsWith(this, stringOrExpr as Expr); + startsWith(prefix: Expr): BooleanExpr; + startsWith(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('starts_with', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); } /** @@ -1027,13 +870,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'filename' field ends with ".txt" - * Field.of("filename").endsWith(".txt"); + * field("filename").endsWith(".txt"); * ``` * * @param suffix The postfix to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: string): EndsWith; + endsWith(suffix: string): BooleanExpr; /** * Creates an expression that checks if a string ends with a given postfix (represented as an @@ -1041,18 +884,18 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'url' field ends with the value of the 'extension' field - * Field.of("url").endsWith(Field.of("extension")); + * field("url").endsWith(field("extension")); * ``` * * @param suffix The postfix expression to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: Expr): EndsWith; - endsWith(stringOrExpr: string | Expr): EndsWith { - if (typeof stringOrExpr === 'string') { - return new EndsWith(this, Constant.of(stringOrExpr)); - } - return new EndsWith(this, stringOrExpr as Expr); + endsWith(suffix: Expr): BooleanExpr; + endsWith(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('ends_with', [ + this, + valueToDefaultExpr(stringOrExpr) + ]); } /** @@ -1060,13 +903,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Convert the 'name' field to lowercase - * Field.of("name").toLower(); + * field("name").toLower(); * ``` * * @return A new `Expr` representing the lowercase string. */ - toLower(): ToLower { - return new ToLower(this); + toLower(): FunctionExpr { + return new FunctionExpr('to_lower', [this]); } /** @@ -1074,13 +917,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Convert the 'title' field to uppercase - * Field.of("title").toUpper(); + * field("title").toUpper(); * ``` * * @return A new `Expr` representing the uppercase string. */ - toUpper(): ToUpper { - return new ToUpper(this); + toUpper(): FunctionExpr { + return new FunctionExpr('to_upper', [this]); } /** @@ -1088,13 +931,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Trim whitespace from the 'userInput' field - * Field.of("userInput").trim(); + * field("userInput").trim(); * ``` * * @return A new `Expr` representing the trimmed string. */ - trim(): Trim { - return new Trim(this); + trim(): FunctionExpr { + return new FunctionExpr('trim', [this]); } /** @@ -1102,17 +945,20 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * Field.of("firstName").strConcat(Constant.of(" "), Field.of("lastName")); + * field("firstName").strConcat(constant(" "), field("lastName")); * ``` * - * @param elements The expressions (typically strings) to concatenate. + * @param secondString The additional expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or string literals to concatenate. * @return A new `Expr` representing the concatenated string. */ - strConcat(...elements: Array): StrConcat { - const exprs = elements.map(e => - typeof e === 'string' ? Constant.of(e) : (e as Expr) - ); - return new StrConcat(this, exprs); + strConcat( + secondString: Expr | string, + ...otherStrings: Array + ): FunctionExpr { + const elements = [secondString, ...otherStrings]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpr('str_concat', [this, ...exprs]); } /** @@ -1120,13 +966,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Reverse the value of the 'myString' field. - * Field.of("myString").reverse(); + * field("myString").reverse(); * ``` * * @return A new {@code Expr} representing the reversed string. */ - reverse(): Reverse { - return new Reverse(this); + reverse(): FunctionExpr { + return new FunctionExpr('reverse', [this]); } /** @@ -1134,14 +980,14 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Replace the first occurrence of "hello" with "hi" in the 'message' field - * Field.of("message").replaceFirst("hello", "hi"); + * field("message").replaceFirst("hello", "hi"); * ``` * * @param find The substring to search for. * @param replace The substring to replace the first occurrence of 'find' with. * @return A new {@code Expr} representing the string with the first occurrence replaced. */ - replaceFirst(find: string, replace: string): ReplaceFirst; + replaceFirst(find: string, replace: string): FunctionExpr; /** * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring, @@ -1149,23 +995,20 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field - * Field.of("message").replaceFirst(Field.of("findField"), Field.of("replaceField")); + * field("message").replaceFirst(field("findField"), field("replaceField")); * ``` * * @param find The expression representing the substring to search for. * @param replace The expression representing the substring to replace the first occurrence of 'find' with. * @return A new {@code Expr} representing the string with the first occurrence replaced. */ - replaceFirst(find: Expr, replace: Expr): ReplaceFirst; - replaceFirst(find: Expr | string, replace: Expr | string): ReplaceFirst { - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceFirst( + replaceFirst(find: Expr, replace: Expr): FunctionExpr; + replaceFirst(find: Expr | string, replace: Expr | string): FunctionExpr { + return new FunctionExpr('replace_first', [ this, - normalizedFind as Expr, - normalizedReplace as Expr - ); + valueToDefaultExpr(find), + valueToDefaultExpr(replace) + ]); } /** @@ -1173,14 +1016,14 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Replace all occurrences of "hello" with "hi" in the 'message' field - * Field.of("message").replaceAll("hello", "hi"); + * field("message").replaceAll("hello", "hi"); * ``` * * @param find The substring to search for. * @param replace The substring to replace all occurrences of 'find' with. * @return A new {@code Expr} representing the string with all occurrences replaced. */ - replaceAll(find: string, replace: string): ReplaceAll; + replaceAll(find: string, replace: string): FunctionExpr; /** * Creates an expression that replaces all occurrences of a substring within this string expression with another substring, @@ -1188,23 +1031,20 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field - * Field.of("message").replaceAll(Field.of("findField"), Field.of("replaceField")); + * field("message").replaceAll(field("findField"), field("replaceField")); * ``` * * @param find The expression representing the substring to search for. * @param replace The expression representing the substring to replace all occurrences of 'find' with. * @return A new {@code Expr} representing the string with all occurrences replaced. */ - replaceAll(find: Expr, replace: Expr): ReplaceAll; - replaceAll(find: Expr | string, replace: Expr | string): ReplaceAll { - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceAll( + replaceAll(find: Expr, replace: Expr): FunctionExpr; + replaceAll(find: Expr | string, replace: Expr | string): FunctionExpr { + return new FunctionExpr('replace_all', [ this, - normalizedFind as Expr, - normalizedReplace as Expr - ); + valueToDefaultExpr(find), + valueToDefaultExpr(replace) + ]); } /** @@ -1212,13 +1052,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the length of the 'myString' field in bytes. - * Field.of("myString").byteLength(); + * field("myString").byteLength(); * ``` * * @return A new {@code Expr} representing the length of the string in bytes. */ - byteLength(): ByteLength { - return new ByteLength(this); + byteLength(): FunctionExpr { + return new FunctionExpr('byte_length', [this]); } /** @@ -1226,14 +1066,14 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Get the 'city' value from the 'address' map field - * Field.of("address").mapGet("city"); + * 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): MapGet { - return new MapGet(this, subfield); + mapGet(subfield: string): FunctionExpr { + return new FunctionExpr('map_get', [this, constant(subfield)]); } /** @@ -1242,13 +1082,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Count the total number of products - * Field.of("productId").count().as("totalProducts"); + * field("productId").count().as("totalProducts"); * ``` * - * @return A new `Accumulator` representing the 'count' aggregation. + * @return A new `AggregateFunction` representing the 'count' aggregation. */ - count(): Count { - return new Count(this, false); + count(): AggregateFunction { + return new AggregateFunction('count', [this]); } /** @@ -1256,13 +1096,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the total revenue from a set of orders - * Field.of("orderAmount").sum().as("totalRevenue"); + * field("orderAmount").sum().as("totalRevenue"); * ``` * - * @return A new `Accumulator` representing the 'sum' aggregation. + * @return A new `AggregateFunction` representing the 'sum' aggregation. */ - sum(): Sum { - return new Sum(this, false); + sum(): AggregateFunction { + return new AggregateFunction('sum', [this]); } /** @@ -1271,13 +1111,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the average age of users - * Field.of("age").avg().as("averageAge"); + * field("age").avg().as("averageAge"); * ``` * - * @return A new `Accumulator` representing the 'avg' aggregation. + * @return A new `AggregateFunction` representing the 'avg' aggregation. */ - avg(): Avg { - return new Avg(this, false); + avg(): AggregateFunction { + return new AggregateFunction('avg', [this]); } /** @@ -1285,13 +1125,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Find the lowest price of all products - * Field.of("price").minimum().as("lowestPrice"); + * field("price").minimum().as("lowestPrice"); * ``` * - * @return A new `Accumulator` representing the 'min' aggregation. + * @return A new `AggregateFunction` representing the 'min' aggregation. */ - minimum(): Minimum { - return new Minimum(this, false); + minimum(): AggregateFunction { + return new AggregateFunction('minimum', [this]); } /** @@ -1299,13 +1139,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Find the highest score in a leaderboard - * Field.of("score").maximum().as("highestScore"); + * field("score").maximum().as("highestScore"); * ``` * - * @return A new `Accumulator` representing the 'max' aggregation. + * @return A new `AggregateFunction` representing the 'max' aggregation. */ - maximum(): Maximum { - return new Maximum(this, false); + maximum(): AggregateFunction { + return new AggregateFunction('maximum', [this]); } /** @@ -1313,31 +1153,22 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Returns the larger value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMaximum(Function.currentTimestamp()); - * ``` - * - * @param other The expression to compare with. - * @return A new {@code Expr} representing the logical max operation. - */ - logicalMaximum(other: Expr): LogicalMaximum; - - /** - * Creates an expression that returns the larger value between this expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'value' field and 10. - * Field.of("value").logicalMaximum(10); + * field("timestamp").logicalMaximum(Function.currentTimestamp()); * ``` * - * @param other The constant value to compare with. + * @param second The second expression or literal to compare with. + * @param others Optional additional expressions or literals to compare with. * @return A new {@code Expr} representing the logical max operation. */ - logicalMaximum(other: any): LogicalMaximum; - logicalMaximum(other: any): LogicalMaximum { - if (other instanceof Expr) { - return new LogicalMaximum(this, other as Expr); - } - return new LogicalMaximum(this, Constant.of(other)); + logicalMaximum( + second: Expr | any, + ...others: Array + ): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('logical_maximum', [ + this, + ...values.map(valueToDefaultExpr) + ]); } /** @@ -1345,31 +1176,22 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Returns the smaller value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMinimum(Function.currentTimestamp()); - * ``` - * - * @param other The expression to compare with. - * @return A new {@code Expr} representing the logical min operation. - */ - logicalMinimum(other: Expr): LogicalMinimum; - - /** - * Creates an expression that returns the smaller value between this expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'value' field and 10. - * Field.of("value").logicalMinimum(10); + * field("timestamp").logicalMinimum(Function.currentTimestamp()); * ``` * - * @param other The constant value to compare with. + * @param second The second expression or literal to compare with. + * @param others Optional additional expressions or literals to compare with. * @return A new {@code Expr} representing the logical min operation. */ - logicalMinimum(other: any): LogicalMinimum; - logicalMinimum(other: any): LogicalMinimum { - if (other instanceof Expr) { - return new LogicalMinimum(this, other as Expr); - } - return new LogicalMinimum(this, Constant.of(other)); + logicalMinimum( + second: Expr | any, + ...others: Array + ): FunctionExpr { + const values = [second, ...others]; + return new FunctionExpr('logical_min', [ + this, + ...values.map(valueToDefaultExpr) + ]); } /** @@ -1377,13 +1199,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Get the vector length (dimension) of the field 'embedding'. - * Field.of("embedding").vectorLength(); + * field("embedding").vectorLength(); * ``` * * @return A new {@code Expr} representing the length of the vector. */ - vectorLength(): VectorLength { - return new VectorLength(this); + vectorLength(): FunctionExpr { + return new FunctionExpr('vector_length', [this]); } /** @@ -1391,46 +1213,27 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * Field.of("userVector").cosineDistance(Field.of("itemVector")); + * field("userVector").cosineDistance(field("itemVector")); * ``` * - * @param other The other vector (represented as an Expr) to compare against. + * @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(other: Expr): CosineDistance; + cosineDistance(vectorExpression: Expr): FunctionExpr; /** * Calculates the Cosine distance between two vectors. * * ```typescript * // Calculate the Cosine distance between the 'location' field and a target location - * Field.of("location").cosineDistance(new VectorValue([37.7749, -122.4194])); + * field("location").cosineDistance(new VectorValue([37.7749, -122.4194])); * ``` * - * @param other The other vector (as a VectorValue) to compare against. + * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Cosine* distance between the two vectors. */ - cosineDistance(other: VectorValue): CosineDistance; - /** - * Calculates the Cosine distance between two vectors. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * Field.of("location").cosineDistance([37.7749, -122.4194]); - * ``` - * - * @param other The other vector (as an array of numbers) to compare against. - * @return A new `Expr` representing the Cosine distance between the two vectors. - */ - cosineDistance(other: number[]): CosineDistance; - cosineDistance(other: Expr | VectorValue | number[]): CosineDistance { - if (other instanceof Expr) { - return new CosineDistance(this, other as Expr); - } else { - return new CosineDistance( - this, - Constant.vector(other as VectorValue | number[]) - ); - } + cosineDistance(vector: VectorValue | number[]): FunctionExpr; + cosineDistance(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)]); } /** @@ -1438,97 +1241,90 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct([0.5, 0.8, 0.2]); + * field("features").dotProduct([0.5, 0.8, 0.2]); * ``` * - * @param other The other vector (as an array of numbers) to calculate with. + * @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(other: Expr): DotProduct; + dotProduct(vectorExpression: Expr): FunctionExpr; /** * Calculates the dot product between two vectors. * * ```typescript * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct(new VectorValue([0.5, 0.8, 0.2])); + * field("features").dotProduct(new VectorValue([0.5, 0.8, 0.2])); * ``` * - * @param other The other vector (as an array of numbers) to calculate with. + * @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(other: VectorValue): DotProduct; + dotProduct(vector: VectorValue | number[]): FunctionExpr; + dotProduct(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('dot_product', [this, vectorToExpr(other)]); + } /** - * Calculates the dot product between two vectors. + * Calculates the Euclidean distance between two vectors. * * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct([0.5, 0.8, 0.2]); + * // Calculate the Euclidean distance between the 'location' field and a target location + * field("location").euclideanDistance([37.7749, -122.4194]); * ``` * - * @param other The other vector (as an array of numbers) to calculate with. - * @return A new `Expr` representing the dot product between the two vectors. + * @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. */ - dotProduct(other: number[]): DotProduct; - dotProduct(other: Expr | VectorValue | number[]): DotProduct { - if (other instanceof Expr) { - return new DotProduct(this, other as Expr); - } else { - return new DotProduct( - this, - Constant.vector(other as VectorValue | number[]) - ); - } - } + euclideanDistance(vectorExpression: Expr): FunctionExpr; /** * Calculates the Euclidean distance between two vectors. * * ```typescript * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance([37.7749, -122.4194]); + * field("location").euclideanDistance(new VectorValue([37.7749, -122.4194])); * ``` * - * @param other The other vector (as an array of numbers) to calculate with. + * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Euclidean distance between the two vectors. */ - euclideanDistance(other: Expr): EuclideanDistance; + euclideanDistance(vector: VectorValue | number[]): FunctionExpr; + euclideanDistance(other: Expr | VectorValue | number[]): FunctionExpr { + return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)]); + } /** - * Calculates the Euclidean distance between two vectors. + * @beta + * + * Calculates the Manhattan distance between the result of this expression and another VectorValue. * * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance(new VectorValue([37.7749, -122.4194])); + * // Calculate the Manhattan distance between the 'location' field and a target location + * field("location").manhattanDistance(new VectorValue([37.7749, -122.4194])); * ``` * - * @param other The other vector (as a VectorValue) to compare against. - * @return A new `Expr` representing the Euclidean distance between the two vectors. + * @param vector The other vector (as a VectorValue) to compare against. + * @return A new {@code Expr} representing the Manhattan distance between the two vectors. */ - euclideanDistance(other: VectorValue): EuclideanDistance; + manhattanDistance(vector: VectorValue | number[]): FunctionExpr; /** - * Calculates the Euclidean distance between two vectors. + * @beta + * + * Calculates the Manhattan distance between two vector expressions. * * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance([37.7749, -122.4194]); + * // Calculate the Manhattan distance between two vector fields: 'pointA' and 'pointB' + * field("pointA").manhattanDistance(field("pointB")); * ``` * - * @param other The other vector (as an array of numbers) to compare against. - * @return A new `Expr` representing the Euclidean distance between the two vectors. + * @param vectorExpression The other vector (represented as an Expr) to compare against. + * @return A new {@code Expr} representing the Manhattan distance between the two vectors. */ - euclideanDistance(other: number[]): EuclideanDistance; - euclideanDistance(other: Expr | VectorValue | number[]): EuclideanDistance { - if (other instanceof Expr) { - return new EuclideanDistance(this, other as Expr); - } else { - return new EuclideanDistance( - this, - Constant.vector(other as VectorValue | number[]) - ); - } + manhattanDistance(vectorExpression: Expr): FunctionExpr; + manhattanDistance(other: Expr | number[] | VectorValue): FunctionExpr { + return new FunctionExpr('manhattan_distance', [this, vectorToExpr(other)]); } /** @@ -1537,13 +1333,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Interpret the 'microseconds' field as microseconds since epoch. - * Field.of("microseconds").unixMicrosToTimestamp(); + * field("microseconds").unixMicrosToTimestamp(); * ``` * * @return A new {@code Expr} representing the timestamp. */ - unixMicrosToTimestamp(): UnixMicrosToTimestamp { - return new UnixMicrosToTimestamp(this); + unixMicrosToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_micros_to_timestamp', [this]); } /** @@ -1551,13 +1347,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Convert the 'timestamp' field to microseconds since epoch. - * Field.of("timestamp").timestampToUnixMicros(); + * field("timestamp").timestampToUnixMicros(); * ``` * * @return A new {@code Expr} representing the number of microseconds since epoch. */ - timestampToUnixMicros(): TimestampToUnixMicros { - return new TimestampToUnixMicros(this); + timestampToUnixMicros(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_micros', [this]); } /** @@ -1566,13 +1362,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Interpret the 'milliseconds' field as milliseconds since epoch. - * Field.of("milliseconds").unixMillisToTimestamp(); + * field("milliseconds").unixMillisToTimestamp(); * ``` * * @return A new {@code Expr} representing the timestamp. */ - unixMillisToTimestamp(): UnixMillisToTimestamp { - return new UnixMillisToTimestamp(this); + unixMillisToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_millis_to_timestamp', [this]); } /** @@ -1580,13 +1376,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Convert the 'timestamp' field to milliseconds since epoch. - * Field.of("timestamp").timestampToUnixMillis(); + * field("timestamp").timestampToUnixMillis(); * ``` * * @return A new {@code Expr} representing the number of milliseconds since epoch. */ - timestampToUnixMillis(): TimestampToUnixMillis { - return new TimestampToUnixMillis(this); + timestampToUnixMillis(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_millis', [this]); } /** @@ -1595,13 +1391,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Interpret the 'seconds' field as seconds since epoch. - * Field.of("seconds").unixSecondsToTimestamp(); + * field("seconds").unixSecondsToTimestamp(); * ``` * * @return A new {@code Expr} representing the timestamp. */ - unixSecondsToTimestamp(): UnixSecondsToTimestamp { - return new UnixSecondsToTimestamp(this); + unixSecondsToTimestamp(): FunctionExpr { + return new FunctionExpr('unix_seconds_to_timestamp', [this]); } /** @@ -1609,13 +1405,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Convert the 'timestamp' field to seconds since epoch. - * Field.of("timestamp").timestampToUnixSeconds(); + * field("timestamp").timestampToUnixSeconds(); * ``` * * @return A new {@code Expr} representing the number of seconds since epoch. */ - timestampToUnixSeconds(): TimestampToUnixSeconds { - return new TimestampToUnixSeconds(this); + timestampToUnixSeconds(): FunctionExpr { + return new FunctionExpr('timestamp_to_unix_seconds', [this]); } /** @@ -1623,21 +1419,21 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. - * Field.of("timestamp").timestampAdd(Field.of("unit"), Field.of("amount")); + * field("timestamp").timestampAdd(field("unit"), field("amount")); * ``` * * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampAdd(unit: Expr, amount: Expr): TimestampAdd; + timestampAdd(unit: Expr, amount: Expr): FunctionExpr; /** * Creates an expression that adds a specified amount of time to this timestamp expression. * * ```typescript * // Add 1 day to the 'timestamp' field. - * Field.of("timestamp").timestampAdd("day", 1); + * field("timestamp").timestampAdd("day", 1); * ``` * * @param unit The unit of time to add (e.g., "day", "hour"). @@ -1647,7 +1443,7 @@ export abstract class Expr implements ProtoSerializable, UserData { timestampAdd( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): TimestampAdd; + ): FunctionExpr; timestampAdd( unit: | Expr @@ -1658,15 +1454,12 @@ export abstract class Expr implements ProtoSerializable, UserData { | 'hour' | 'day', amount: Expr | number - ): TimestampAdd { - const normalizedUnit = typeof unit === 'string' ? Constant.of(unit) : unit; - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampAdd( + ): FunctionExpr { + return new FunctionExpr('timestamp_add', [ this, - normalizedUnit as Expr, - normalizedAmount as Expr - ); + valueToDefaultExpr(unit), + valueToDefaultExpr(amount) + ]); } /** @@ -1674,21 +1467,21 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * Field.of("timestamp").timestampSub(Field.of("unit"), Field.of("amount")); + * field("timestamp").timestampSub(field("unit"), field("amount")); * ``` * * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampSub(unit: Expr, amount: Expr): TimestampSub; + timestampSub(unit: Expr, amount: Expr): FunctionExpr; /** * Creates an expression that subtracts a specified amount of time from this timestamp expression. * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * Field.of("timestamp").timestampSub("day", 1); + * field("timestamp").timestampSub("day", 1); * ``` * * @param unit The unit of time to subtract (e.g., "day", "hour"). @@ -1698,7 +1491,7 @@ export abstract class Expr implements ProtoSerializable, UserData { timestampSub( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): TimestampSub; + ): FunctionExpr; timestampSub( unit: | Expr @@ -1709,131 +1502,587 @@ export abstract class Expr implements ProtoSerializable, UserData { | 'hour' | 'day', amount: Expr | number - ): TimestampSub { - const normalizedUnit = typeof unit === 'string' ? Constant.of(unit) : unit; - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampSub( + ): FunctionExpr { + return new FunctionExpr('timestamp_sub', [ this, - normalizedUnit as Expr, - normalizedAmount as Expr - ); + valueToDefaultExpr(unit), + valueToDefaultExpr(amount) + ]); } /** - * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * @beta + * + * Creates an expression that applies a bitwise AND operation between this expression and a constant. * * ```typescript - * // Sort documents by the 'name' field in ascending order - * pipeline().collection("users") - * .sort(Field.of("name").ascending()); + * // Calculate the bitwise AND of 'field1' and 0xFF. + * field("field1").bitAnd(0xFF); * ``` * - * @return A new `Ordering` for ascending sorting. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. */ - ascending(): Ordering { - return ascending(this); + bitAnd(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise AND operation between two expressions. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * field("field1").bitAnd(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. + */ + bitAnd(bitsExpression: Expr): FunctionExpr; + bitAnd(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_and', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); } /** - * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * @beta + * + * Creates an expression that applies a bitwise OR operation between this expression and a constant. * * ```typescript - * // Sort documents by the 'createdAt' field in descending order - * firestore.pipeline().collection("users") - * .sort(Field.of("createdAt").descending()); + * // Calculate the bitwise OR of 'field1' and 0xFF. + * field("field1").bitOr(0xFF); * ``` * - * @return A new `Ordering` for descending sorting. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. */ - descending(): Ordering { - return descending(this); + bitOr(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise OR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * field("field1").bitOr(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. + */ + bitOr(bitsExpression: Expr): FunctionExpr; + bitOr(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_or', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); } /** - * Assigns an alias to this expression. + * @beta * - * Aliases are useful for renaming fields in the output of a stage or for giving meaningful - * names to calculated values. + * Creates an expression that applies a bitwise XOR operation between this expression and a constant. * * ```typescript - * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. - * firestore.pipeline().collection("items") - * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice")); + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * field("field1").bitXor(0xFF); * ``` * - * @param name The alias to assign to this expression. - * @return A new {@link ExprWithAlias} that wraps this - * expression and associates it with the provided alias. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. */ - as(name: string): ExprWithAlias { - return new ExprWithAlias(this, name); + bitXor(otherBits: number | Bytes): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise XOR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * field("field1").bitXor(field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. + */ + bitXor(bitsExpression: Expr): FunctionExpr; + bitXor(bitsOrExpression: number | Expr | Bytes): FunctionExpr { + return new FunctionExpr('bit_xor', [ + this, + valueToDefaultExpr(bitsOrExpression) + ]); } /** - * @private - * @internal + * @beta + * + * Creates an expression that applies a bitwise NOT operation to this expression. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * field("field1").bitNot(); + * ``` + * + * @return A new {@code Expr} representing the bitwise NOT operation. */ - abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; + bitNot(): FunctionExpr { + return new FunctionExpr('bit_not', [this]); + } /** - * @private - * @internal + * @beta + * + * Creates an expression that applies a bitwise left shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * field("field1").bitLeftShift(2); + * ``` + * + * @param y The operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ - abstract _readUserData(dataReader: UserDataReader): void; -} - -/** - * @beta - * - * An interface that represents a selectable expression. - */ -export abstract class Selectable extends Expr { - selectable: true = true; -} - -/** - * @beta - * - * An interface that represents a filter condition. - */ -export abstract class FilterCondition extends Expr { - filterable: true = true; -} - -/** - * @beta - * - * An interface that represents an accumulator. - */ -export abstract class Accumulator extends Expr { - accumulator: true = true; - + bitLeftShift(y: number): FunctionExpr; /** - * @private - * @internal + * @beta + * + * Creates an expression that applies a bitwise left shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * field("field1").bitLeftShift(field("field2")); + * ``` + * + * @param numberExpr The operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ - abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; + bitLeftShift(numberExpr: Expr): FunctionExpr; + bitLeftShift(numberExpr: number | Expr): FunctionExpr { + return new FunctionExpr('bit_left_shift', [ + this, + valueToDefaultExpr(numberExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that applies a bitwise right shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * field("field1").bitRightShift(2); + * ``` + * + * @param right The operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ + bitRightShift(y: number): FunctionExpr; + /** + * @beta + * + * Creates an expression that applies a bitwise right shift operation to this expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * field("field1").bitRightShift(field("field2")); + * ``` + * + * @param numberExpr The operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. + */ + bitRightShift(numberExpr: Expr): FunctionExpr; + bitRightShift(numberExpr: number | Expr): FunctionExpr { + return new FunctionExpr('bit_right_shift', [ + this, + valueToDefaultExpr(numberExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that returns the document ID from a path. + * + * ```typescript + * // Get the document ID from a path. + * field("__path__").documentId(); + * ``` + * + * @return A new {@code Expr} representing the documentId operation. + */ + documentId(): FunctionExpr { + return new FunctionExpr('document_id', [this]); + } + + /** + * @beta + * + * Creates an expression that returns a substring of the results of this expression. + * + * @param position Index of the first character of the substring. + * @param length Length of the substring. If not provided, the substring will + * end at the end of the input. + */ + substr(position: number, length?: number): FunctionExpr; + + /** + * @beta + * + * Creates an expression that returns a substring of the results of this expression. + * + * @param position An expression returning the index of the first character of the substring. + * @param length An expression returning the length of the substring. If not provided the + * substring will end at the end of the input. + */ + substr(position: Expr, length?: Expr): FunctionExpr; + substr(position: Expr | number, length?: Expr | number): FunctionExpr { + const positionExpr = valueToDefaultExpr(position); + if (length === undefined) { + return new FunctionExpr('substr', [this, positionExpr]); + } else { + return new FunctionExpr('substr', [ + this, + positionExpr, + valueToDefaultExpr(length) + ]); + } + } + + /** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and returns the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the 'tags' field array at index `1`. + * field('tags').arrayOffset(1); + * ``` + * + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ + arrayOffset(offset: number): FunctionExpr; + + /** + * @beta + * Creates an expression that indexes into an array from the beginning or end + * and returns the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * field('tags').arrayOffset(field('favoriteTag')); + * ``` + * + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. + */ + arrayOffset(offsetExpr: Expr): FunctionExpr; + arrayOffset(offset: Expr | number): FunctionExpr { + return new FunctionExpr('array_offset', [this, valueToDefaultExpr(offset)]); + } + + /** + * @beta + * + * Creates an expression that checks if a given expression produces an error. + * + * ```typescript + * // Check if the result of a calculation is an error + * field("title").arrayContains(1).isError(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isError' check. + */ + isError(): BooleanExpr { + return new BooleanExpr('is_error', [this]); + } + + /** + * @beta + * + * Creates an expression that returns the result of the `catchExpr` argument + * if there is an error, else return the result of this expression. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // the entire title field if the array is empty or the field is another type. + * field("title").arrayOffset(0).ifError(field("title")); + * ``` + * + * @param catchExpr The catch expression that will be evaluated and + * returned if this expression produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchExpr: Expr): FunctionExpr; + + /** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of this expression. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // "Default Title" + * field("title").arrayOffset(0).ifError("Default Title"); + * ``` + * + * @param catchValue The value that will be returned if this expression + * produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchValue: any): FunctionExpr; + ifError(catchValue: any): FunctionExpr { + return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)]); + } + + /** + * @beta + * + * Creates an expression that returns `true` if the result of this expression + * is absent. Otherwise, returns `false` even if the value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * field("value").isAbsent(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isAbsent' check. + */ + isAbsent(): BooleanExpr { + return new BooleanExpr('is_absent', [this]); + } + + /** + * @beta + * + * Creates an expression that checks if tbe result of an expression is not null. + * + * ```typescript + * // Check if the value of the 'name' field is not null + * field("name").isNotNull(); + * ``` + * + * @return A new {@code BooleanExpr} representing the 'isNotNull' check. + */ + isNotNull(): BooleanExpr { + return new BooleanExpr('is_not_null', [this]); + } + + /** + * @beta + * + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NOT NaN + * field("value").divide(0).isNotNan(); + * ``` + * + * @return A new {@code Expr} representing the 'isNaN' check. + */ + isNotNan(): BooleanExpr { + return new BooleanExpr('is_not_nan', [this]); + } + + /** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating this expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * map({foo: 'bar', baz: true}).mapRemove('baz'); + * ``` + * + * @param key The name of the key to remove from the input map. + * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. + */ + mapRemove(key: string): FunctionExpr; + /** + * @beta + * + * Creates an expression that removes a key from the map produced by evaluating this expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * map({foo: 'bar', baz: true}).mapRemove(constant('baz')); + * ``` + * + * @param keyExpr An expression that produces the name of the key to remove from the input map. + * @returns A new {@code FirestoreFunction} representing the 'mapRemove' operation. + */ + mapRemove(keyExpr: Expr): FunctionExpr; + mapRemove(stringExpr: Expr | string): FunctionExpr { + return new FunctionExpr('map_remove', [ + this, + valueToDefaultExpr(stringExpr) + ]); + } + + /** + * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * field('settings').mapMerge({ enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param secondMap A required second map to merge. Represented as a literal or + * an expression that returns a map. + * @param otherMaps Optional additional maps to merge. Each map is represented + * as a literal or an expression that returns a map. + * + * @returns A new {@code FirestoreFunction} representing the 'mapMerge' operation. + */ + mapMerge( + secondMap: Record | Expr, + ...otherMaps: Array | Expr> + ): FunctionExpr { + const secondMapExpr = valueToDefaultExpr(secondMap); + const otherMapExprs = otherMaps.map(valueToDefaultExpr); + return new FunctionExpr('map_merge', [ + this, + secondMapExpr, + ...otherMapExprs + ]); + } + + /** + * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * + * ```typescript + * // Sort documents by the 'name' field in ascending order + * pipeline().collection("users") + * .sort(field("name").ascending()); + * ``` + * + * @return A new `Ordering` for ascending sorting. + */ + ascending(): Ordering { + return ascending(this); + } + + /** + * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * + * ```typescript + * // Sort documents by the 'createdAt' field in descending order + * firestore.pipeline().collection("users") + * .sort(field("createdAt").descending()); + * ``` + * + * @return A new `Ordering` for descending sorting. + */ + descending(): Ordering { + return descending(this); + } + + /** + * Assigns an alias to this expression. + * + * Aliases are useful for renaming fields in the output of a stage or for giving meaningful + * names to calculated values. + * + * ```typescript + * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. + * firestore.pipeline().collection("items") + * .addFields(field("price").multiply(field("quantity")).as("totalPrice")); + * ``` + * + * @param name The alias to assign to this expression. + * @return A new {@link ExprWithAlias} that wraps this + * expression and associates it with the provided alias. + */ + as(name: string): ExprWithAlias { + return new ExprWithAlias(this, name); + } } /** * @beta * - * An accumulator target, which is an expression with an alias that also implements the Accumulator interface. + * An interface that represents a selectable expression. */ -export type AccumulatorTarget = ExprWithAlias; +export interface Selectable { + selectable: true; + readonly alias: string; + readonly expr: Expr; +} /** * @beta + * + * A class that represents an aggregate function. */ -export class ExprWithAlias extends Selectable { - exprType: ExprType = 'ExprWithAlias'; - selectable = true as const; +export class AggregateFunction implements ProtoValueSerializable, UserData { + exprType: ExprType = 'AggregateFunction'; - constructor(readonly expr: T, readonly alias: string) { - super(); + constructor(private name: string, private params: Expr[]) {} + + /** + * Assigns an alias to this AggregateFunction. The alias specifies the name that + * the aggregated value will have in the output document. + * + * ```typescript + * // Calculate the average price of all items and assign it the alias "averagePrice". + * firestore.pipeline().collection("items") + * .aggregate(field("price").avg().as("averagePrice")); + * ``` + * + * @param name The alias to assign to this AggregateFunction. + * @return A new {@link AggregateWithAlias} that wraps this + * AggregateFunction and associates it with the provided alias. + */ + as(name: string): AggregateWithAlias { + return new AggregateWithAlias(this, name); } + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + functionValue: { + name: this.name, + args: this.params.map(p => p._toProto(serializer)) + } + }; + } + + _protoValueType = 'ProtoValue' as const; + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader): void { + this.params.forEach(expr => { + return expr._readUserData(dataReader); + }); + } +} + +/** + * @beta + * + * An AggregateFunction with alias. + */ +export class AggregateWithAlias + implements UserData, ProtoSerializable +{ + constructor(readonly aggregate: AggregateFunction, readonly alias: string) {} + /** * @private * @internal @@ -1842,6 +2091,24 @@ export class ExprWithAlias extends Selectable { throw new Error('ExprWithAlias should not be serialized directly.'); } + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader): void { + this.aggregate._readUserData(dataReader); + } +} + +/** + * @beta + */ +export class ExprWithAlias implements Selectable, UserData { + exprType: ExprType = 'ExprWithAlias'; + selectable = true as const; + + constructor(readonly expr: Expr, readonly alias: string) {} + /** * @private * @internal @@ -1856,6 +2123,7 @@ export class ExprWithAlias extends Selectable { */ class ListOfExprs extends Expr { exprType: ExprType = 'ListOfExprs'; + constructor(private exprs: Expr[]) { super(); } @@ -1893,106 +2161,36 @@ class ListOfExprs extends Expr { * * ```typescript * // Create a Field instance for the 'name' field - * const nameField = Field.of("name"); + * const nameField = field("name"); * * // Create a Field instance for a nested field 'address.city' - * const cityField = Field.of("address.city"); + * const cityField = field("address.city"); * ``` */ -export class Field extends Selectable { - exprType: ExprType = 'Field'; +export class Field extends Expr implements Selectable { + readonly exprType: ExprType = 'Field'; selectable = true as const; - private constructor( - private fieldPath: InternalFieldPath, - private pipeline: Pipeline | null = null - ) { - super(); - } - /** - * 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.of("title"); - * - * // Create a Field instance for a nested field 'author.firstName' - * const authorFirstNameField = Field.of("author.firstName"); - * ``` - * - * @param name The path to the field. - * @return A new {@code Field} instance representing the specified field. - */ - static of(name: string): Field; - static of(path: FieldPath): Field; - static of( - pipelineOrName: Pipeline | string | FieldPath, - name?: string - ): Field { - if (typeof pipelineOrName === 'string') { - if (DOCUMENT_KEY_NAME === pipelineOrName) { - return new Field(documentId()._internalPath); - } - return new Field(fieldPathFromArgument('of', pipelineOrName)); - } else if (pipelineOrName instanceof FieldPath) { - if (documentId().isEqual(pipelineOrName)) { - return new Field(documentId()._internalPath); - } - return new Field(pipelineOrName._internalPath); - } else { - return new Field( - fieldPathFromArgument('of', name!), - pipelineOrName as Pipeline - ); - } - } - - fieldName(): string { - return this.fieldPath.canonicalString(); - } - - /** - * @private * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - fieldReferenceValue: this.fieldPath.canonicalString() - }; - } - - /** * @private - * @internal + * @hideconstructor + * @param fieldPath */ - _readUserData(dataReader: UserDataReader): void {} -} - -/** - * @beta - */ -export class Fields extends Selectable { - exprType: ExprType = 'Field'; - selectable = true as const; - - private constructor(private fields: Field[]) { + constructor(private fieldPath: InternalFieldPath) { super(); } - static of(name: string, ...others: string[]): Fields { - return new Fields([Field.of(name), ...others.map(Field.of)]); + fieldName(): string { + return this.fieldPath.canonicalString(); } - static ofAll(): Fields { - return new Fields([]); + get alias(): string { + return this.fieldName(); } - fieldList(): Field[] { - return this.fields.map(f => f); + get expr(): Expr { + return this; } /** @@ -2001,9 +2199,7 @@ export class Fields extends Selectable { */ _toProto(serializer: JsonProtoSerializer): ProtoValue { return { - arrayValue: { - values: this.fields.map(f => f._toProto(serializer)) - } + fieldReferenceValue: this.fieldPath.canonicalString() }; } @@ -2011,8 +2207,39 @@ export class Fields extends Selectable { * @private * @internal */ - _readUserData(dataReader: UserDataReader): void { - this.fields.forEach(expr => expr._readUserData(dataReader)); + _readUserData(dataReader: UserDataReader): void {} +} + +/** + * Creates a {@code Field} instance representing the field at the given path. + * + * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * ```typescript + * // Create a Field instance for the 'title' field + * const titleField = field("title"); + * + * // Create a Field instance for a nested field 'author.firstName' + * const authorFirstNameField = field("author.firstName"); + * ``` + * + * @param name The path to the field. + * @return A new {@code Field} instance representing the specified field. + */ +export function field(name: string): Field; +export function field(path: FieldPath): Field; +export function field(nameOrPath: string | FieldPath): Field { + if (typeof nameOrPath === 'string') { + if (DOCUMENT_KEY_NAME === nameOrPath) { + return new Field(documentId()._internalPath); + } + return new Field(fieldPathFromArgument('of', nameOrPath)); + } else { + if (documentId().isEqual(nameOrPath)) { + return new Field(documentId()._internalPath); + } + return new Field(nameOrPath._internalPath); } } @@ -2025,158 +2252,25 @@ export class Fields extends Selectable { * * ```typescript * // Create a Constant instance for the number 10 - * const ten = Constant.of(10); + * const ten = constant(10); * * // Create a Constant instance for the string "hello" - * const hello = Constant.of("hello"); + * const hello = constant("hello"); * ``` */ export class Constant extends Expr { - exprType: ExprType = 'Constant'; + readonly exprType: ExprType = 'Constant'; private _protoValue?: ProtoValue; - private constructor(private value: any) { - super(); - } - - /** - * Creates a `Constant` instance for a number value. - * - * @param value The number value. - * @return A new `Constant` instance. - */ - static of(value: number): Constant; - - /** - * Creates a `Constant` instance for a string value. - * - * @param value The string value. - * @return A new `Constant` instance. - */ - static of(value: string): Constant; - - /** - * Creates a `Constant` instance for a boolean value. - * - * @param value The boolean value. - * @return A new `Constant` instance. - */ - static of(value: boolean): Constant; - - /** - * Creates a `Constant` instance for a null value. - * - * @param value The null value. - * @return A new `Constant` instance. - */ - static of(value: null): Constant; - - /** - * Creates a `Constant` instance for an undefined value. - * @private - * @internal - * - * @param value The undefined value. - * @return A new `Constant` instance. - */ - static of(value: undefined): Constant; - - /** - * Creates a `Constant` instance for a GeoPoint value. - * - * @param value The GeoPoint value. - * @return A new `Constant` instance. - */ - static of(value: GeoPoint): Constant; - - /** - * Creates a `Constant` instance for a Timestamp value. - * - * @param value The Timestamp value. - * @return A new `Constant` instance. - */ - static of(value: Timestamp): Constant; - - /** - * Creates a `Constant` instance for a Date value. - * - * @param value The Date value. - * @return A new `Constant` instance. - */ - static of(value: Date): Constant; - - /** - * Creates a `Constant` instance for a Bytes value. - * - * @param value The Bytes value. - * @return A new `Constant` instance. - */ - static of(value: Bytes): Constant; - - /** - * Creates a `Constant` instance for a DocumentReference value. - * - * @param value The DocumentReference value. - * @return A new `Constant` instance. - */ - static of(value: DocumentReference): Constant; - /** - * Creates a `Constant` instance for a Firestore proto value. - * For internal use only. * @private * @internal - * @param value The Firestore proto value. - * @return A new `Constant` instance. - */ - static of(value: ProtoValue): Constant; - - /** - * Creates a `Constant` instance for an array value. - * - * @param value The array value. - * @return A new `Constant` instance. + * @hideconstructor + * @param value The value of the constant. */ - static of(value: any[]): Constant; - - /** - * Creates a `Constant` instance for a map value. - * - * @param value The map value. - * @return A new `Constant` instance. - */ - static of(value: Record): Constant; - - /** - * Creates a `Constant` instance for a VectorValue value. - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ - static of(value: VectorValue): Constant; - - static of(value: any): Constant { - return new Constant(value); - } - - /** - * Creates a `Constant` instance for a VectorValue value. - * - * ```typescript - * // Create a Constant instance for a vector value - * const vectorConstant = Constant.ofVector([1, 2, 3]); - * ``` - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ - static vector(value: number[] | VectorValue): Constant { - if (value instanceof VectorValue) { - return new Constant(value); - } else { - return new Constant(new VectorValue(value as number[])); - } + constructor(private value: any) { + super(); } /** @@ -2211,10 +2305,8 @@ export class Constant extends Expr { 'Constant.of' ); - if (isFirestoreValue(this.value)) { - // Special case where value is a proto value. - // This can occur when converting a Query to Pipeline. - this._protoValue = this.value; + if (isFirestoreValue(this._protoValue)) { + return; } else if (this.value === undefined) { // TODO(pipeline) how should we treat the value of `undefined`? this._protoValue = parseData(null, context)!; @@ -2225,790 +2317,1213 @@ export class Constant extends Expr { } /** - * @beta + * Creates a `Constant` instance for a number value. * - * This class defines the base class for Firestore {@link Pipeline} functions, which can be evaluated within pipeline - * execution. + * @param value The number value. + * @return A new `Constant` instance. + */ +export function constant(value: number): Constant; + +/** + * Creates a `Constant` instance for a string value. * - * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link eq}, - * or the methods on {@link Expr} ({@link Expr#eq}, {@link Expr#lt}, etc) to construct new Function instances. + * @param value The string value. + * @return A new `Constant` instance. */ -export class FirestoreFunction extends Expr { - exprType: ExprType = 'Function'; - constructor(private name: string, private params: Expr[]) { - super(); - } +export function constant(value: string): Constant; - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - functionValue: { - name: this.name, - args: this.params.map(p => p._toProto(serializer)) - } - }; - } +/** + * Creates a `Constant` instance for a boolean value. + * + * @param value The boolean value. + * @return A new `Constant` instance. + */ +export function constant(value: boolean): Constant; - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.params.forEach(expr => expr._readUserData(dataReader)); - } -} +/** + * Creates a `Constant` instance for a null value. + * + * @param value The null value. + * @return A new `Constant` instance. + */ +export function constant(value: null): Constant; /** - * @beta + * Creates a `Constant` instance for an undefined value. + * @private + * @internal + * + * @param value The undefined value. + * @return A new `Constant` instance. */ -export class Add extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('add', [left, right]); - } -} +export function constant(value: undefined): Constant; /** - * @beta + * Creates a `Constant` instance for a GeoPoint value. + * + * @param value The GeoPoint value. + * @return A new `Constant` instance. */ -export class Subtract extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('subtract', [left, right]); - } -} +export function constant(value: GeoPoint): Constant; /** - * @beta + * Creates a `Constant` instance for a Timestamp value. + * + * @param value The Timestamp value. + * @return A new `Constant` instance. */ -export class Multiply extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('multiply', [left, right]); - } -} +export function constant(value: Timestamp): Constant; /** - * @beta + * Creates a `Constant` instance for a Date value. + * + * @param value The Date value. + * @return A new `Constant` instance. */ -export class Divide extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('divide', [left, right]); - } -} +export function constant(value: Date): Constant; /** - * @beta + * Creates a `Constant` instance for a Bytes value. + * + * @param value The Bytes value. + * @return A new `Constant` instance. */ -export class Mod extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('mod', [left, right]); - } -} +export function constant(value: Bytes): Constant; -// /** -// * @beta -// */ -// export class BitAnd extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_and', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitOr extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_or', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitXor extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_xor', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitNot extends FirestoreFunction { -// constructor(private operand: Expr) { -// super('bit_not', [operand]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitLeftShift extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_left_shift', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitRightShift extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_right_shift', [left, right]); -// } -// } - -/** - * @beta - */ -export class Eq extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('eq', [left, right]); - } - filterable = true as const; -} +/** + * Creates a `Constant` instance for a DocumentReference value. + * + * @param value The DocumentReference value. + * @return A new `Constant` instance. + */ +export function constant(value: DocumentReference): Constant; /** - * @beta + * 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 class Neq extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('neq', [left, right]); - } - filterable = true as const; +export function constant(value: ProtoValue): Constant; + +/** + * Creates a `Constant` instance for a VectorValue value. + * + * @param value The VectorValue value. + * @return A new `Constant` instance. + */ +export function constant(value: VectorValue): Constant; + +export function constant(value: any): Constant { + return new Constant(value); } /** - * @beta + * Creates a `Constant` instance for a VectorValue value. + * + * ```typescript + * // Create a Constant instance for a vector value + * const vectorConstant = constantVector([1, 2, 3]); + * ``` + * + * @param value The VectorValue value. + * @return A new `Constant` instance. */ -export class Lt extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('lt', [left, right]); +export function constantVector(value: number[] | VectorValue): Constant { + if (value instanceof VectorValue) { + return new Constant(value); + } else { + return new Constant(new VectorValue(value as number[])); } - filterable = true as const; } /** - * @beta + * Internal only + * @internal + * @private */ -export class Lte extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('lte', [left, right]); +export class MapValue extends Expr { + constructor(private plainObject: Map) { + super(); + } + + exprType: ExprType = 'Constant'; + + _readUserData(dataReader: UserDataReader): void { + this.plainObject.forEach(expr => { + expr._readUserData(dataReader); + }); + } + + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return toMapValue(serializer, this.plainObject); } - filterable = true as const; } /** * @beta + * + * This class defines the base class for Firestore {@link Pipeline} functions, which can be evaluated within pipeline + * execution. + * + * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link eq}, + * or the methods on {@link Expr} ({@link Expr#eq}, {@link Expr#lt}, etc) to construct new Function instances. */ -export class Gt extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('gt', [left, right]); +export class FunctionExpr extends Expr { + readonly exprType: ExprType = 'Function'; + + constructor(private name: string, private params: Expr[]) { + super(); } - filterable = true as const; -} -/** - * @beta - */ -export class Gte extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('gte', [left, right]); + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return { + functionValue: { + name: this.name, + args: this.params.map(p => p._toProto(serializer)) + } + }; + } + + /** + * @private + * @internal + */ + _readUserData(dataReader: UserDataReader): void { + this.params.forEach(expr => { + return expr._readUserData(dataReader); + }); } - filterable = true as const; } /** * @beta + * + * An interface that represents a filter condition. */ -export class ArrayConcat extends FirestoreFunction { - constructor(private array: Expr, private elements: Expr[]) { - super('array_concat', [array, ...elements]); +export class BooleanExpr extends FunctionExpr { + filterable: true = true; + + /** + * Creates an aggregation that finds the count of input documents satisfying + * this boolean expression. + * + * ```typescript + * // Find the count of documents with a score greater than 90 + * field("score").gt(90).countIf().as("highestScore"); + * ``` + * + * @return A new `AggregateFunction` representing the 'countIf' aggregation. + */ + countIf(): AggregateFunction { + return new AggregateFunction('count_if', [this]); + } + + /** + * Creates an expression that negates this boolean expression. + * + * ```typescript + * // Find documents where the 'tags' field does not contain 'completed' + * field("tags").arrayContains("completed").not(); + * ``` + * + * @return A new {@code Expr} representing the negated filter condition. + */ + not(): BooleanExpr { + return new BooleanExpr('not', [this]); } } /** * @beta + * Creates an aggregation that counts the number of stage inputs where the provided + * boolean expression evaluates to true. + * + * ```typescript + * // Count the number of documents where 'is_active' field equals true + * countif(field("is_active").eq(true)).as("numActiveDocuments"); + * ``` + * + * @param booleanExpr - The boolean expression to evaluate on each input. + * @returns A new `AggregateFunction` representing the 'countif' aggregation. */ -export class ArrayReverse extends FirestoreFunction { - constructor(private array: Expr) { - super('array_reverse', [array]); - } +export function countIf(booleanExpr: BooleanExpr): AggregateFunction { + return booleanExpr.countIf(); } /** * @beta + * Creates an expression that return a pseudo-random value of type double in the + * range of [0, 1), inclusive of 0 and exclusive of 1. + * + * @returns A new `Expr` representing the 'rand' function. */ -export class ArrayContains - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private element: Expr) { - super('array_contains', [array, element]); - } - filterable = true as const; +export function rand(): FunctionExpr { + return new FunctionExpr('rand', []); } /** * @beta + * + * Creates an expression that applies a bitwise AND operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 0xFF. + * bitAnd("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. */ -export class ArrayContainsAll - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private values: Expr[]) { - super('array_contains_all', [array, new ListOfExprs(values)]); - } - filterable = true as const; -} - +export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise AND operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * bitAnd("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. */ -export class ArrayContainsAny - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private values: Expr[]) { - super('array_contains_any', [array, new ListOfExprs(values)]); - } - filterable = true as const; -} - +export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise AND operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 0xFF. + * bitAnd(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise AND operation. */ -export class ArrayLength extends FirestoreFunction { - constructor(private array: Expr) { - super('array_length', [array]); - } -} - +export function bitAnd( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise AND operation between two expressions. + * + * ```typescript + * // Calculate the bitwise AND of 'field1' and 'field2'. + * bitAnd(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise AND operation. */ -export class ArrayElement extends FirestoreFunction { - constructor() { - super('array_element', []); - } +export function bitAnd( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitAnd( + fieldOrExpression: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOfOrExpr(fieldOrExpression).bitAnd( + valueToDefaultExpr(bitsOrExpression) + ); } /** * @beta + * + * Creates an expression that applies a bitwise OR operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 0xFF. + * bitOr("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. */ -export class EqAny extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private others: Expr[]) { - super('eq_any', [left, new ListOfExprs(others)]); - } - filterable = true as const; -} - +export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise OR operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * bitOr("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. */ -export class NotEqAny extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private others: Expr[]) { - super('not_eq_any', [left, new ListOfExprs(others)]); - } - filterable = true as const; -} - +export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise OR operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 0xFF. + * bitOr(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise OR operation. */ -export class IsNan extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('is_nan', [expr]); - } - filterable = true as const; -} - +export function bitOr( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise OR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise OR of 'field1' and 'field2'. + * bitOr(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise OR operation. */ -export class Exists extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('exists', [expr]); - } - filterable = true as const; +export function bitOr( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitOr( + fieldOrExpression: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOfOrExpr(fieldOrExpression).bitOr( + valueToDefaultExpr(bitsOrExpression) + ); } /** * @beta + * + * Creates an expression that applies a bitwise XOR operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * bitXor("field1", 0xFF); + * ``` + * + * @param field The left operand field name. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. */ -export class Not extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('not', [expr]); - } - filterable = true as const; -} - +export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise XOR operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * bitXor("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param bitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. */ -export class And extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterCondition[]) { - super('and', conditions); - } - - filterable = true as const; -} - +export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise XOR operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 0xFF. + * bitXor(field("field1"), 0xFF); + * ``` + * + * @param bitsExpression An expression returning bits. + * @param otherBits A constant representing bits. + * @return A new {@code Expr} representing the bitwise XOR operation. */ -export class Or extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterCondition[]) { - super('or', conditions); - } - filterable = true as const; -} - +export function bitXor( + bitsExpression: Expr, + otherBits: number | Bytes +): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise XOR operation between two expressions. + * + * ```typescript + * // Calculate the bitwise XOR of 'field1' and 'field2'. + * bitXor(field("field1"), field("field2")); + * ``` + * + * @param bitsExpression An expression that returns bits when evaluated. + * @param otherBitsExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise XOR operation. */ -export class Xor extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterCondition[]) { - super('xor', conditions); - } - filterable = true as const; +export function bitXor( + bitsExpression: Expr, + otherBitsExpression: Expr +): FunctionExpr; +export function bitXor( + fieldOrExpression: string | Expr, + bitsOrExpression: number | Expr | Bytes +): FunctionExpr { + return fieldOfOrExpr(fieldOrExpression).bitXor( + valueToDefaultExpr(bitsOrExpression) + ); } /** * @beta + * + * Creates an expression that applies a bitwise NOT operation to a field. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * bitNot("field1"); + * ``` + * + * @param field The operand field name. + * @return A new {@code Expr} representing the bitwise NOT operation. */ -export class Cond extends FirestoreFunction { - constructor( - private condition: FilterCondition, - private thenExpr: Expr, - private elseExpr: Expr - ) { - super('cond', [condition, thenExpr, elseExpr]); - } - filterable = true as const; -} - +export function bitNot(field: string): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise NOT operation to an expression. + * + * ```typescript + * // Calculate the bitwise NOT of 'field1'. + * bitNot(field("field1")); + * ``` + * + * @param bitsValueExpression An expression that returns bits when evaluated. + * @return A new {@code Expr} representing the bitwise NOT operation. */ -export class LogicalMaximum extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('logical_maximum', [left, right]); - } +export function bitNot(bitsValueExpression: Expr): FunctionExpr; +export function bitNot(bits: string | Expr): FunctionExpr { + return fieldOfOrExpr(bits).bitNot(); } /** * @beta + * + * Creates an expression that applies a bitwise left shift operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * bitLeftShift("field1", 2); + * ``` + * + * @param field The left operand field name. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ -export class LogicalMinimum extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('logical_minimum', [left, right]); - } -} - +export function bitLeftShift(field: string, y: number): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise left shift operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * bitLeftShift("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param numberExpr The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ -export class Reverse extends FirestoreFunction { - constructor(private value: Expr) { - super('reverse', [value]); - } -} - +export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise left shift operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 2 bits. + * bitLeftShift(field("field1"), 2); + * ``` + * + * @param xValue An expression returning bits. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ -export class ReplaceFirst extends FirestoreFunction { - constructor(private value: Expr, private find: Expr, private replace: Expr) { - super('replace_first', [value, find, replace]); - } -} - +export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise left shift operation between two expressions. + * + * ```typescript + * // Calculate the bitwise left shift of 'field1' by 'field2' bits. + * bitLeftShift(field("field1"), field("field2")); + * ``` + * + * @param xValue An expression returning bits. + * @param right The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise left shift operation. */ -export class ReplaceAll extends FirestoreFunction { - constructor(private value: Expr, private find: Expr, private replace: Expr) { - super('replace_all', [value, find, replace]); - } +export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; +export function bitLeftShift( + xValue: string | Expr, + numberExpr: number | Expr +): FunctionExpr { + return fieldOfOrExpr(xValue).bitLeftShift(valueToDefaultExpr(numberExpr)); } /** * @beta + * + * Creates an expression that applies a bitwise right shift operation between a field and a constant. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * bitRightShift("field1", 2); + * ``` + * + * @param left The left operand field name. + * @param right The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. */ -export class CharLength extends FirestoreFunction { - constructor(private value: Expr) { - super('char_length', [value]); - } -} - +export function bitRightShift(field: string, y: number): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise right shift operation between a field and an expression. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * bitRightShift("field1", field("field2")); + * ``` + * + * @param field The left operand field name. + * @param numberExpr The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. */ -export class ByteLength extends FirestoreFunction { - constructor(private value: Expr) { - super('byte_length', [value]); - } -} - +export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise right shift operation between an expression and a constant. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 2 bits. + * bitRightShift(field("field1"), 2); + * ``` + * + * @param xValue An expression returning bits. + * @param y The right operand constant representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. */ -export class Like extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private pattern: Expr) { - super('like', [expr, pattern]); - } - filterable = true as const; -} - +export function bitRightShift(xValue: Expr, y: number): FunctionExpr; /** * @beta + * + * Creates an expression that applies a bitwise right shift operation between two expressions. + * + * ```typescript + * // Calculate the bitwise right shift of 'field1' by 'field2' bits. + * bitRightShift(field("field1"), field("field2")); + * ``` + * + * @param xValue An expression returning bits. + * @param right The right operand expression representing the number of bits to shift. + * @return A new {@code Expr} representing the bitwise right shift operation. */ -export class RegexContains - extends FirestoreFunction - implements FilterCondition -{ - constructor(private expr: Expr, private pattern: Expr) { - super('regex_contains', [expr, pattern]); - } - filterable = true as const; +export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; +export function bitRightShift( + xValue: string | Expr, + numberExpr: number | Expr +): FunctionExpr { + return fieldOfOrExpr(xValue).bitRightShift(valueToDefaultExpr(numberExpr)); } /** * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index 1. + * arrayOffset('tags', 1); + * ``` + * + * @param arrayField The name of the array field. + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. */ -export class RegexMatch extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private pattern: Expr) { - super('regex_match', [expr, pattern]); - } - filterable = true as const; -} +export function arrayOffset(arrayField: string, offset: number): FunctionExpr; /** * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * arrayOffset('tags', field('favoriteTag')); + * ``` + * + * @param arrayField The name of the array field. + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. */ -export class StrContains extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private substring: Expr) { - super('str_contains', [expr, substring]); - } - filterable = true as const; -} +export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; /** * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index 1. + * arrayOffset(field('tags'), 1); + * ``` + * + * @param arrayExpression An Expr evaluating to an array. + * @param offset The index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. */ -export class StartsWith extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private prefix: Expr) { - super('starts_with', [expr, prefix]); - } - filterable = true as const; -} +export function arrayOffset( + arrayExpression: Expr, + offset: number +): FunctionExpr; /** * @beta + * Creates an expression that indexes into an array from the beginning or end + * and return the element. If the offset exceeds the array length, an error is + * returned. A negative offset, starts from the end. + * + * ```typescript + * // Return the value in the tags field array at index specified by field + * // 'favoriteTag'. + * arrayOffset(field('tags'), field('favoriteTag')); + * ``` + * + * @param arrayExpression An Expr evaluating to an array. + * @param offsetExpr An Expr evaluating to the index of the element to return. + * @return A new Expr representing the 'arrayOffset' operation. */ -export class EndsWith extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private suffix: Expr) { - super('ends_with', [expr, suffix]); - } - filterable = true as const; +export function arrayOffset( + arrayExpression: Expr, + offsetExpr: Expr +): FunctionExpr; +export function arrayOffset( + array: Expr | string, + offset: Expr | number +): FunctionExpr { + return fieldOfOrExpr(array).arrayOffset(valueToDefaultExpr(offset)); } /** * @beta + * Creates an Expr that returns a map of all values in the current expression context. + * + * @return A new {@code Expr} representing the 'current_context' function. */ -export class ToLower extends FirestoreFunction { - constructor(private expr: Expr) { - super('to_lower', [expr]); - } +export function currentContext(): FunctionExpr { + return new FunctionExpr('current_context', []); } /** * @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 class ToUpper extends FirestoreFunction { - constructor(private expr: Expr) { - super('to_upper', [expr]); - } +export function isError(value: Expr): BooleanExpr { + return value.isError(); } /** * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // the entire title field if the array is empty or the field is another type. + * ifError(field("title").arrayOffset(0), field("title")); + * ``` + * + * @param tryExpr The try expression. + * @param catchExpr The catch expression that will be evaluated and + * returned if the tryExpr produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. */ -export class Trim extends FirestoreFunction { - constructor(private expr: Expr) { - super('trim', [expr]); - } -} +export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * ```typescript + * // Returns the first item in the title field arrays, or returns + * // "Default Title" + * ifError(field("title").arrayOffset(0), "Default Title"); + * ``` + * + * @param tryExpr The try expression. + * @param catchValue The value that will be returned if the tryExpr produces an + * error. + * @return A new {@code Expr} representing the 'ifError' operation. */ -export class StrConcat extends FirestoreFunction { - constructor(private first: Expr, private rest: Expr[]) { - super('str_concat', [first, ...rest]); - } +export function ifError(tryExpr: Expr, catchValue: any): FunctionExpr; +export function ifError(tryExpr: Expr, catchValue: any): FunctionExpr { + return tryExpr.ifError(valueToDefaultExpr(catchValue)); } /** * @beta + * + * Creates an expression that returns `true` if a value is absent. Otherwise, + * returns `false` even if the value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * isAbsent(field("value")); + * ``` + * + * @param value The expression to check. + * @return A new {@code Expr} representing the 'isAbsent' check. */ -export class MapGet extends FirestoreFunction { - constructor(map: Expr, name: string) { - super('map_get', [map, Constant.of(name)]); - } -} +export function isAbsent(value: Expr): BooleanExpr; /** * @beta + * + * Creates an expression that returns `true` if a field is absent. Otherwise, + * returns `false` even if the field value is `null`. + * + * ```typescript + * // Check if the field `value` is absent. + * isAbsent("value"); + * ``` + * + * @param field The field to check. + * @return A new {@code Expr} representing the 'isAbsent' check. */ -export class Count extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr | undefined, private distinct: boolean) { - super('count', value === undefined ? [] : [value]); - } +export function isAbsent(field: string): BooleanExpr; +export function isAbsent(value: Expr | string): BooleanExpr { + return fieldOfOrExpr(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 class Sum extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('sum', [value]); - } -} +export function isNull(value: Expr): BooleanExpr; /** * @beta + * + * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). + * + * ```typescript + * // Check if the result of a calculation is NaN + * isNaN("value"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNaN' check. */ -export class Avg extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('avg', [value]); - } +export function isNull(value: string): BooleanExpr; +export function isNull(value: Expr | string): BooleanExpr { + return fieldOfOrExpr(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 class Minimum extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('minimum', [value]); - } -} +export function isNotNull(value: Expr): BooleanExpr; /** * @beta + * + * Creates an expression that checks if tbe value of a field is not null. + * + * ```typescript + * // Check if the value of the 'name' field is not null + * isNotNull("name"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNaN' check. */ -export class Maximum extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('maximum', [value]); - } +export function isNotNull(value: string): BooleanExpr; +export function isNotNull(value: Expr | string): BooleanExpr { + return fieldOfOrExpr(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 class CosineDistance extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('cosine_distance', [vector1, vector2]); - } -} +export function isNotNan(value: Expr): BooleanExpr; /** * @beta + * + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). + * + * ```typescript + * // Check if the value of a field is NOT NaN + * isNotNaN("value"); + * ``` + * + * @param value The name of the field to check. + * @return A new {@code Expr} representing the 'isNotNaN' check. */ -export class DotProduct extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('dot_product', [vector1, vector2]); - } +export function isNotNan(value: string): BooleanExpr; +export function isNotNan(value: Expr | string): BooleanExpr { + return fieldOfOrExpr(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 class EuclideanDistance extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('euclidean_distance', [vector1, vector2]); - } -} - +export function mapRemove(mapField: string, key: string): FunctionExpr; /** * @beta + * + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * mapRemove(map({foo: 'bar', baz: true}), 'baz'); + * ``` + * + * @param mapExpr An expression return a map value. + * @param key The name of the key to remove from the input map. */ -export class VectorLength extends FirestoreFunction { - constructor(private value: Expr) { - super('vector_length', [value]); - } -} - +export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; /** * @beta + * + * Creates an expression that removes a key from the map at the specified field name. + * + * ``` + * // Removes the key 'city' field from the map in the address field of the input document. + * mapRemove('address', constant('city')); + * ``` + * + * @param mapField The name of a field containing a map value. + * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export class UnixMicrosToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_micros_to_timestamp', [input]); - } -} - +export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; /** * @beta + * + * Creates an expression that removes a key from the map produced by evaluating an expression. + * + * ``` + * // Removes the key 'baz' from the input map. + * mapRemove(map({foo: 'bar', baz: true}), constant('baz')); + * ``` + * + * @param mapExpr An expression return a map value. + * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export class TimestampToUnixMicros extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_micros', [input]); - } +export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; + +export function mapRemove( + mapExpr: Expr | string, + stringExpr: Expr | string +): FunctionExpr { + return fieldOfOrExpr(mapExpr).mapRemove(valueToDefaultExpr(stringExpr)); } /** * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * mapMerge('settings', { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param mapField Name of a field containing a map value that will be merged. + * @param secondMap A required second map to merge. Represented as a literal or + * an expression that returns a map. + * @param otherMaps Optional additional maps to merge. Each map is represented + * as a literal or an expression that returns a map. */ -export class UnixMillisToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_millis_to_timestamp', [input]); - } -} +export function mapMerge( + mapField: string, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr; /** * @beta + * + * Creates an expression that merges multiple map values. + * + * ``` + * // Merges the map in the settings field with, a map literal, and a map in + * // that is conditionally returned by another expression + * mapMerge(field('settings'), { enabled: true }, cond(field('isAdmin'), { admin: true}, {}) + * ``` + * + * @param firstMap An expression or literal map 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 class TimestampToUnixMillis extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_millis', [input]); - } +export function mapMerge( + firstMap: Record | Expr, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr; + +export function mapMerge( + firstMap: string | Record | Expr, + secondMap: Record | Expr, + ...otherMaps: Array | Expr> +): FunctionExpr { + const secondMapExpr = valueToDefaultExpr(secondMap); + const otherMapExprs = otherMaps.map(valueToDefaultExpr); + return fieldOfOrExpr(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 class UnixSecondsToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_seconds_to_timestamp', [input]); - } -} +export function documentIdFunction( + documentPath: string | DocumentReference +): FunctionExpr; /** * @beta + * + * Creates an expression that returns the document ID from a path. + * + * ```typescript + * // Get the document ID from a path. + * documentId(field("__path__")); + * ``` + * + * @return A new {@code Expr} representing the documentId operation. */ -export class TimestampToUnixSeconds extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_seconds', [input]); - } +export function documentIdFunction(documentPathExpr: Expr): FunctionExpr; + +export function documentIdFunction( + documentPath: Expr | string | DocumentReference +): FunctionExpr { + // @ts-ignore + const documentPathExpr = valueToDefaultExpr(documentPath); + return documentPathExpr.documentId(); } /** * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param field The name of a field containing a string or byte array to compute the substring from. + * @param position Index of the first character of the substring. + * @param length Length of the substring. */ -export class TimestampAdd extends FirestoreFunction { - constructor( - private timestamp: Expr, - private unit: Expr, - private amount: Expr - ) { - super('timestamp_add', [timestamp, unit, amount]); - } -} +export function substr( + field: string, + position: number, + length?: number +): FunctionExpr; /** * @beta + * + * Creates an expression that returns a substring of a string or byte array. + * + * @param input An expression returning a string or byte array to compute the substring from. + * @param position Index of the first character of the substring. + * @param length Length of the substring. */ -export class TimestampSub extends FirestoreFunction { - constructor( - private timestamp: Expr, - private unit: Expr, - private amount: Expr - ) { - super('timestamp_sub', [timestamp, unit, amount]); - } -} +export function substr( + input: Expr, + position: number, + length?: number +): FunctionExpr; /** * @beta * - * Creates an expression that adds two expressions together. - * - * ```typescript - * // Add the value of the 'quantity' field and the 'reserve' field. - * add(Field.of("quantity"), Field.of("reserve")); - * ``` + * Creates an expression that returns a substring of a string or byte array. * - * @param left The first expression to add. - * @param right The second expression to add. - * @return A new {@code Expr} representing the addition operation. + * @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 add(left: Expr, right: Expr): Add; +export function substr( + field: string, + position: Expr, + length?: Expr +): FunctionExpr; /** * @beta * - * Creates an expression that adds an expression to a constant value. - * - * ```typescript - * // Add 5 to the value of the 'age' field - * add(Field.of("age"), 5); - * ``` + * Creates an expression that returns a substring of a string or byte array. * - * @param left The expression to add to. - * @param right The constant value to add. - * @return A new {@code Expr} representing the addition operation. + * @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 add(left: Expr, right: any): Add; +export function substr( + input: Expr, + position: Expr, + length?: Expr +): FunctionExpr; + +export function substr( + field: Expr | string, + position: Expr | number, + length?: Expr | number +): FunctionExpr { + const fieldExpr = fieldOfOrExpr(field); + const positionExpr = valueToDefaultExpr(position); + const lengthExpr = + length === undefined ? undefined : valueToDefaultExpr(length); + return fieldExpr.substr(positionExpr, lengthExpr); +} /** * @beta * - * Creates an expression that adds a field's value to an expression. + * Creates an expression that adds two expressions together. * * ```typescript * // Add the value of the 'quantity' field and the 'reserve' field. - * add("quantity", Field.of("reserve")); + * add(field("quantity"), field("reserve")); * ``` * - * @param left The field name to add to. - * @param right The expression to add. + * @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(left: string, right: Expr): Add; +export function add( + first: Expr, + second: Expr | any, + ...others: Array +): FunctionExpr; /** * @beta * - * Creates an expression that adds a field's value to a constant value. + * Creates an expression that adds a field's value to an expression. * * ```typescript - * // Add 5 to the value of the 'age' field - * add("age", 5); + * // Add the value of the 'quantity' field and the 'reserve' field. + * add("quantity", field("reserve")); * ``` * - * @param left The field name to add to. - * @param right The constant value to add. + * @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(left: string, right: any): Add; -export function add(left: Expr | string, right: Expr | any): Add { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Add(normalizedLeft, normalizedRight); +export function add( + fieldName: string, + second: Expr | any, + ...others: Array +): FunctionExpr; + +export function add( + first: Expr | string, + second: Expr | any, + ...others: Array +): FunctionExpr { + return fieldOfOrExpr(first).add( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); } /** @@ -3018,14 +3533,14 @@ export function add(left: Expr | string, right: Expr | any): Add { * * ```typescript * // Subtract the 'discount' field from the 'price' field - * subtract(Field.of("price"), Field.of("discount")); + * subtract(field("price"), field("discount")); * ``` * * @param left The expression to subtract from. * @param right The expression to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(left: Expr, right: Expr): Subtract; +export function subtract(left: Expr, right: Expr): FunctionExpr; /** * @beta @@ -3034,14 +3549,14 @@ export function subtract(left: Expr, right: Expr): Subtract; * * ```typescript * // Subtract the constant value 2 from the 'value' field - * subtract(Field.of("value"), 2); + * subtract(field("value"), 2); * ``` * - * @param left The expression to subtract from. - * @param right The constant value to subtract. + * @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(left: Expr, right: any): Subtract; +export function subtract(expression: Expr, value: any): FunctionExpr; /** * @beta @@ -3050,14 +3565,14 @@ export function subtract(left: Expr, right: any): Subtract; * * ```typescript * // Subtract the 'discount' field from the 'price' field - * subtract("price", Field.of("discount")); + * subtract("price", field("discount")); * ``` * - * @param left The field name to subtract from. - * @param right The expression to subtract. + * @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(left: string, right: Expr): Subtract; +export function subtract(fieldName: string, expression: Expr): FunctionExpr; /** * @beta @@ -3069,15 +3584,15 @@ export function subtract(left: string, right: Expr): Subtract; * subtract("total", 20); * ``` * - * @param left The field name to subtract from. - * @param right The constant value to subtract. + * @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(left: string, right: any): Subtract; -export function subtract(left: Expr | string, right: Expr | any): Subtract { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Subtract(normalizedLeft, normalizedRight); +export function subtract(fieldName: string, value: any): FunctionExpr; +export function subtract(left: Expr | string, right: Expr | any): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.subtract(normalizedRight); } /** @@ -3087,30 +3602,19 @@ export function subtract(left: Expr | string, right: Expr | any): Subtract { * * ```typescript * // Multiply the 'quantity' field by the 'price' field - * multiply(Field.of("quantity"), Field.of("price")); - * ``` - * - * @param left The first expression to multiply. - * @param right The second expression to multiply. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: Expr, right: Expr): Multiply; - -/** - * @beta - * - * Creates an expression that multiplies an expression by a constant value. - * - * ```typescript - * // Multiply the value of the 'price' field by 2 - * multiply(Field.of("price"), 2); + * multiply(field("quantity"), field("price")); * ``` * - * @param left The expression to multiply. - * @param right The constant value to multiply by. + * @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(left: Expr, right: any): Multiply; +export function multiply( + first: Expr, + second: Expr | any, + ...others: Array +): FunctionExpr; /** * @beta @@ -3119,34 +3623,29 @@ export function multiply(left: Expr, right: any): Multiply; * * ```typescript * // Multiply the 'quantity' field by the 'price' field - * multiply("quantity", Field.of("price")); - * ``` - * - * @param left The field name to multiply. - * @param right The expression to multiply by. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: string, right: Expr): Multiply; - -/** - * @beta - * - * Creates an expression that multiplies a field's value by a constant value. - * - * ```typescript - * // Multiply the 'value' field by 2 - * multiply("value", 2); + * multiply("quantity", field("price")); * ``` * - * @param left The field name to multiply. - * @param right The constant value to multiply by. + * @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(left: string, right: any): Multiply; -export function multiply(left: Expr | string, right: Expr | any): Multiply { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Multiply(normalizedLeft, normalizedRight); +export function multiply( + fieldName: string, + second: Expr | any, + ...others: Array +): FunctionExpr; + +export function multiply( + first: Expr | string, + second: Expr | any, + ...others: Array +): FunctionExpr { + return fieldOfOrExpr(first).multiply( + valueToDefaultExpr(second), + ...others.map(valueToDefaultExpr) + ); } /** @@ -3156,14 +3655,14 @@ export function multiply(left: Expr | string, right: Expr | any): Multiply { * * ```typescript * // Divide the 'total' field by the 'count' field - * divide(Field.of("total"), Field.of("count")); + * divide(field("total"), field("count")); * ``` * * @param left The expression to be divided. * @param right The expression to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(left: Expr, right: Expr): Divide; +export function divide(left: Expr, right: Expr): FunctionExpr; /** * @beta @@ -3172,14 +3671,14 @@ export function divide(left: Expr, right: Expr): Divide; * * ```typescript * // Divide the 'value' field by 10 - * divide(Field.of("value"), 10); + * divide(field("value"), 10); * ``` * - * @param left The expression to be divided. - * @param right The constant value to divide by. + * @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(left: Expr, right: any): Divide; +export function divide(expression: Expr, value: any): FunctionExpr; /** * @beta @@ -3188,14 +3687,14 @@ export function divide(left: Expr, right: any): Divide; * * ```typescript * // Divide the 'total' field by the 'count' field - * divide("total", Field.of("count")); + * divide("total", field("count")); * ``` * - * @param left The field name to be divided. - * @param right The expression to divide by. + * @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(left: string, right: Expr): Divide; +export function divide(fieldName: string, expressions: Expr): FunctionExpr; /** * @beta @@ -3207,15 +3706,15 @@ export function divide(left: string, right: Expr): Divide; * divide("value", 10); * ``` * - * @param left The field name to be divided. - * @param right The constant value to divide by. + * @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(left: string, right: any): Divide; -export function divide(left: Expr | string, right: Expr | any): Divide { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Divide(normalizedLeft, normalizedRight); +export function divide(fieldName: string, value: any): FunctionExpr; +export function divide(left: Expr | string, right: Expr | any): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.divide(normalizedRight); } /** @@ -3225,14 +3724,14 @@ export function divide(left: Expr | string, right: Expr | any): Divide { * * ```typescript * // Calculate the remainder of dividing 'field1' by 'field2'. - * mod(Field.of("field1"), Field.of("field2")); + * mod(field("field1"), field("field2")); * ``` * * @param left The dividend expression. * @param right The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: Expr, right: Expr): Mod; +export function mod(left: Expr, right: Expr): FunctionExpr; /** * @beta @@ -3241,14 +3740,14 @@ export function mod(left: Expr, right: Expr): Mod; * * ```typescript * // Calculate the remainder of dividing 'field1' by 5. - * mod(Field.of("field1"), 5); + * mod(field("field1"), 5); * ``` * - * @param left The dividend expression. - * @param right The divisor constant. + * @param expression The dividend expression. + * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: Expr, right: any): Mod; +export function mod(expression: Expr, value: any): FunctionExpr; /** * @beta @@ -3257,14 +3756,14 @@ export function mod(left: Expr, right: any): Mod; * * ```typescript * // Calculate the remainder of dividing 'field1' by 'field2'. - * mod("field1", Field.of("field2")); + * mod("field1", field("field2")); * ``` * - * @param left The dividend field name. - * @param right The divisor expression. + * @param fieldName The dividend field name. + * @param expression The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: string, right: Expr): Mod; +export function mod(fieldName: string, expression: Expr): FunctionExpr; /** * @beta @@ -3276,402 +3775,83 @@ export function mod(left: string, right: Expr): Mod; * mod("field1", 5); * ``` * - * @param left The dividend field name. - * @param right The divisor constant. + * @param fieldName The dividend field name. + * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: string, right: any): Mod; -export function mod(left: Expr | string, right: Expr | any): Mod { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Mod(normalizedLeft, normalizedRight); +export function mod(fieldName: string, value: any): FunctionExpr; +export function mod(left: Expr | string, right: Expr | any): FunctionExpr { + const normalizedLeft = typeof left === 'string' ? field(left) : left; + const normalizedRight = valueToDefaultExpr(right); + return normalizedLeft.mod(normalizedRight); +} + +/** + * @beta + * + * Creates an expression that creates a Firestore map value from an input object. + * + * ```typescript + * // Create a map from the input object and reference the 'baz' field value from the input document. + * map({foo: 'bar', baz: Field.of('baz')}).as('data'); + * ``` + * + * @param elements The input map to evaluate in the expression. + * @return A new {@code Expr} representing the map function. + */ +export function map(elements: Record): FunctionExpr { + const result: any[] = []; + for (const key in elements) { + if (Object.prototype.hasOwnProperty.call(elements, key)) { + const value = elements[key]; + result.push(constant(key)); + result.push(valueToDefaultExpr(value)); + } + } + return new FunctionExpr('map', result); +} + +/** + * Internal use only + * Converts a plainObject to a mapValue in the proto representation, + * rather than a functionValue+map that is the result of the map(...) function. + * This behaves different than constant(plainObject) because it + * traverses the input object, converts values in the object to expressions, + * and calls _readUserData on each of these expressions. + * @private + * @internal + * @param plainObject + */ +export function _mapValue(plainObject: Record): MapValue { + const result: Map = new Map(); + for (const key in plainObject) { + if (Object.prototype.hasOwnProperty.call(plainObject, key)) { + const value = plainObject[key]; + result.set(key, valueToDefaultExpr(value)); + } + } + return new MapValue(result); } -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 'field2'. -// * bitAnd(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: Expr, right: Expr): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 0xFF. -// * bitAnd(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: Expr, right: any): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 'field2'. -// * bitAnd("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: string, right: Expr): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 0xFF. -// * bitAnd("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: string, right: any): BitAnd; -// export function bitAnd(left: Expr | string, right: Expr | any): BitAnd { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitAnd(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 'field2'. -// * bitOr(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: Expr, right: Expr): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 0xFF. -// * bitOr(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: Expr, right: any): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 'field2'. -// * bitOr("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: string, right: Expr): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 0xFF. -// * bitOr("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: string, right: any): BitOr; -// export function bitOr(left: Expr | string, right: Expr | any): BitOr { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitOr(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 'field2'. -// * bitXor(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: Expr, right: Expr): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 0xFF. -// * bitXor(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: Expr, right: any): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 'field2'. -// * bitXor("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: string, right: Expr): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 0xFF. -// * bitXor("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: string, right: any): BitXor; -// export function bitXor(left: Expr | string, right: Expr | any): BitXor { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitXor(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise NOT operation to an expression. -// * -// * ```typescript -// * // Calculate the bitwise NOT of 'field1'. -// * bitNot(Field.of("field1")); -// * ``` -// * -// * @param operand The operand expression. -// * @return A new {@code Expr} representing the bitwise NOT operation. -// */ -// export function bitNot(operand: Expr): BitNot; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise NOT operation to a field. -// * -// * ```typescript -// * // Calculate the bitwise NOT of 'field1'. -// * bitNot("field1"); -// * ``` -// * -// * @param operand The operand field name. -// * @return A new {@code Expr} representing the bitwise NOT operation. -// */ -// export function bitNot(operand: string): BitNot; -// export function bitNot(operand: Expr | string): BitNot { -// const normalizedOperand = -// typeof operand === 'string' ? Field.of(operand) : operand; -// return new BitNot(normalizedOperand); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 'field2' bits. -// * bitLeftShift(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: Expr, right: Expr): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 2 bits. -// * bitLeftShift(Field.of("field1"), 2); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: Expr, right: any): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 'field2' bits. -// * bitLeftShift("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: string, right: Expr): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 2 bits. -// * bitLeftShift("field1", 2); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: string, right: any): BitLeftShift; -// export function bitLeftShift( -// left: Expr | string, -// right: Expr | any -// ): BitLeftShift { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitLeftShift(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 'field2' bits. -// * bitRightShift(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: Expr, right: Expr): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 2 bits. -// * bitRightShift(Field.of("field1"), 2); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: Expr, right: any): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 'field2' bits. -// * bitRightShift("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: string, right: Expr): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 2 bits. -// * bitRightShift("field1", 2); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: string, right: any): BitRightShift; -// export function bitRightShift( -// left: Expr | string, -// right: Expr | any -// ): BitRightShift { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitRightShift(normalizedLeft, normalizedRight); -// } +/** + * @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: any[]): FunctionExpr { + return new FunctionExpr( + 'array', + elements.map(element => valueToDefaultExpr(element)) + ); +} /** * @beta @@ -3680,14 +3860,14 @@ export function mod(left: Expr | string, right: Expr | any): Mod { * * ```typescript * // Check if the 'age' field is equal to an expression - * eq(Field.of("age"), Field.of("minAge").add(10)); + * eq(field("age"), field("minAge").add(10)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: Expr, right: Expr): Eq; +export function eq(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -3696,14 +3876,14 @@ export function eq(left: Expr, right: Expr): Eq; * * ```typescript * // Check if the 'age' field is equal to 21 - * eq(Field.of("age"), 21); + * eq(field("age"), 21); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: Expr, right: any): Eq; +export function eq(expression: Expr, value: any): BooleanExpr; /** * @beta @@ -3712,14 +3892,14 @@ export function eq(left: Expr, right: any): Eq; * * ```typescript * // Check if the 'age' field is equal to the 'limit' field - * eq("age", Field.of("limit")); + * eq("age", field("limit")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param expression The expression to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: string, right: Expr): Eq; +export function eq(fieldName: string, expression: Expr): BooleanExpr; /** * @beta @@ -3731,15 +3911,15 @@ export function eq(left: string, right: Expr): Eq; * eq("city", "London"); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: string, right: any): Eq; -export function eq(left: Expr | string, right: any): Eq { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Eq(leftExpr, rightExpr); +export function eq(fieldName: string, value: any): BooleanExpr; +export function eq(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.eq(rightExpr); } /** @@ -3749,14 +3929,14 @@ export function eq(left: Expr | string, right: any): Eq { * * ```typescript * // Check if the 'status' field is not equal to field 'finalState' - * neq(Field.of("status"), Field.of("finalState")); + * neq(field("status"), field("finalState")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: Expr, right: Expr): Neq; +export function neq(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -3765,14 +3945,14 @@ export function neq(left: Expr, right: Expr): Neq; * * ```typescript * // Check if the 'status' field is not equal to "completed" - * neq(Field.of("status"), "completed"); + * neq(field("status"), "completed"); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: Expr, right: any): Neq; +export function neq(expression: Expr, value: any): BooleanExpr; /** * @beta @@ -3781,14 +3961,14 @@ export function neq(left: Expr, right: any): Neq; * * ```typescript * // Check if the 'status' field is not equal to the value of 'expectedStatus' - * neq("status", Field.of("expectedStatus")); + * neq("status", field("expectedStatus")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param expression The expression to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: string, right: Expr): Neq; +export function neq(fieldName: string, expression: Expr): BooleanExpr; /** * @beta @@ -3800,15 +3980,15 @@ export function neq(left: string, right: Expr): Neq; * neq("country", "USA"); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: string, right: any): Neq; -export function neq(left: Expr | string, right: any): Neq { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Neq(leftExpr, rightExpr); +export function neq(fieldName: string, value: any): BooleanExpr; +export function neq(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.neq(rightExpr); } /** @@ -3818,14 +3998,14 @@ export function neq(left: Expr | string, right: any): Neq { * * ```typescript * // Check if the 'age' field is less than 30 - * lt(Field.of("age"), Field.of("limit")); + * lt(field("age"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: Expr, right: Expr): Lt; +export function lt(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -3834,14 +4014,14 @@ export function lt(left: Expr, right: Expr): Lt; * * ```typescript * // Check if the 'age' field is less than 30 - * lt(Field.of("age"), 30); + * lt(field("age"), 30); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: Expr, right: any): Lt; +export function lt(expression: Expr, value: any): BooleanExpr; /** * @beta @@ -3850,14 +4030,14 @@ export function lt(left: Expr, right: any): Lt; * * ```typescript * // Check if the 'age' field is less than the 'limit' field - * lt("age", Field.of("limit")); + * lt("age", field("limit")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param expression The expression to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: string, right: Expr): Lt; +export function lt(fieldName: string, expression: Expr): BooleanExpr; /** * @beta @@ -3869,15 +4049,15 @@ export function lt(left: string, right: Expr): Lt; * lt("price", 50); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: string, right: any): Lt; -export function lt(left: Expr | string, right: any): Lt { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Lt(leftExpr, rightExpr); +export function lt(fieldName: string, value: any): BooleanExpr; +export function lt(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lt(rightExpr); } /** @@ -3888,14 +4068,14 @@ export function lt(left: Expr | string, right: any): Lt { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(Field.of("quantity"), Field.of("limit")); + * lte(field("quantity"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: Expr, right: Expr): Lte; +export function lte(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -3904,28 +4084,28 @@ export function lte(left: Expr, right: Expr): Lte; * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(Field.of("quantity"), 20); + * lte(field("quantity"), 20); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: Expr, right: any): Lte; +export function lte(expression: Expr, value: any): BooleanExpr; /** * Creates an expression that checks if a field's value is less than or equal to an expression. * * ```typescript * // Check if the 'quantity' field is less than or equal to the 'limit' field - * lte("quantity", Field.of("limit")); + * lte("quantity", field("limit")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param expression The expression to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: string, right: Expr): Lte; +export function lte(fieldName: string, expression: Expr): BooleanExpr; /** * @beta @@ -3937,15 +4117,15 @@ export function lte(left: string, right: Expr): Lte; * lte("score", 70); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: string, right: any): Lte; -export function lte(left: Expr | string, right: any): Lte { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Lte(leftExpr, rightExpr); +export function lte(fieldName: string, value: any): BooleanExpr; +export function lte(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.lte(rightExpr); } /** @@ -3956,14 +4136,14 @@ export function lte(left: Expr | string, right: any): Lte { * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(Field.of("age"), Constant(9).add(9)); + * gt(field("age"), Constant(9).add(9)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: Expr, right: Expr): Gt; +export function gt(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -3972,14 +4152,14 @@ export function gt(left: Expr, right: Expr): Gt; * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(Field.of("age"), 18); + * gt(field("age"), 18); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: Expr, right: any): Gt; +export function gt(expression: Expr, value: any): BooleanExpr; /** * @beta @@ -3988,14 +4168,14 @@ export function gt(left: Expr, right: any): Gt; * * ```typescript * // Check if the value of field 'age' is greater than the value of field 'limit' - * gt("age", Field.of("limit")); + * gt("age", field("limit")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param expression The expression to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: string, right: Expr): Gt; +export function gt(fieldName: string, expression: Expr): BooleanExpr; /** * @beta @@ -4007,15 +4187,15 @@ export function gt(left: string, right: Expr): Gt; * gt("price", 100); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: string, right: any): Gt; -export function gt(left: Expr | string, right: any): Gt { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Gt(leftExpr, rightExpr); +export function gt(fieldName: string, value: any): BooleanExpr; +export function gt(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.gt(rightExpr); } /** @@ -4026,14 +4206,14 @@ export function gt(left: Expr | string, right: any): Gt { * * ```typescript * // Check if the 'quantity' field is greater than or equal to the field "threshold" - * gte(Field.of("quantity"), Field.of("threshold")); + * gte(field("quantity"), field("threshold")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: Expr, right: Expr): Gte; +export function gte(left: Expr, right: Expr): BooleanExpr; /** * @beta @@ -4043,14 +4223,14 @@ export function gte(left: Expr, right: Expr): Gte; * * ```typescript * // Check if the 'quantity' field is greater than or equal to 10 - * gte(Field.of("quantity"), 10); + * gte(field("quantity"), 10); * ``` * - * @param left The expression to compare. - * @param right The constant value to compare to. + * @param expression The expression to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: Expr, right: any): Gte; +export function gte(expression: Expr, value: any): BooleanExpr; /** * @beta @@ -4059,14 +4239,14 @@ export function gte(left: Expr, right: any): Gte; * * ```typescript * // Check if the value of field 'age' is greater than or equal to the value of field 'limit' - * gte("age", Field.of("limit")); + * gte("age", field("limit")); * ``` * - * @param left The field name to compare. - * @param right The expression to compare to. + * @param fieldName The field name to compare. + * @param value The expression to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: string, right: Expr): Gte; +export function gte(fieldName: string, value: Expr): BooleanExpr; /** * @beta @@ -4079,15 +4259,15 @@ export function gte(left: string, right: Expr): Gte; * gte("score", 80); * ``` * - * @param left The field name to compare. - * @param right The constant value to compare to. + * @param fieldName The field name to compare. + * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: string, right: any): Gte; -export function gte(left: Expr | string, right: any): Gte { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Gte(leftExpr, rightExpr); +export function gte(fieldName: string, value: any): BooleanExpr; +export function gte(left: Expr | string, right: any): BooleanExpr { + const leftExpr = left instanceof Expr ? left : field(left); + const rightExpr = valueToDefaultExpr(right); + return leftExpr.gte(rightExpr); } /** @@ -4097,71 +4277,51 @@ export function gte(left: Expr | string, right: any): Gte { * * ```typescript * // Combine the 'items' array with two new item arrays - * arrayConcat(Field.of("items"), [Field.of("newItems"), Field.of("otherItems")]); - * ``` - * - * @param array The array expression to concatenate to. - * @param elements The array expressions to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: Expr, elements: Expr[]): ArrayConcat; - -/** - * @beta - * - * Creates an expression that concatenates an array expression with other arrays and/or values. - * - * ```typescript - * // Combine the 'tags' array with a new array - * arrayConcat(Field.of("tags"), ["newTag1", "newTag2"]); - * ``` - * - * @param array The array expression to concatenate to. - * @param elements The array expressions or single values to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: Expr, elements: any[]): ArrayConcat; - -/** - * @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.of("newItems"), Field.of("otherItems")]); + * arrayConcat(field("items"), [field("newItems"), field("otherItems")]); * ``` * - * @param array The field name containing array values. - * @param elements The array expressions to concatenate. + * @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(array: string, elements: Expr[]): ArrayConcat; +export function arrayConcat( + firstArray: Expr, + secondArray: Expr | any, + ...otherArrays: Array +): FunctionExpr; /** * @beta * - * Creates an expression that concatenates a field's array value with other arrays and/or values. + * Creates an expression that concatenates a field's array value with other arrays. * * ```typescript - * // Combine the 'tags' array with a new array - * arrayConcat("tags", ["newTag1", "newTag2"]); + * // Combine the 'items' array with two new item arrays + * arrayConcat("items", [field("newItems"), field("otherItems")]); * ``` * - * @param array The field name containing array values. - * @param elements The array expressions or single values to concatenate. + * @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(array: string, elements: any[]): ArrayConcat; export function arrayConcat( - array: Expr | string, - elements: any[] -): ArrayConcat { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = elements.map(element => - element instanceof Expr ? element : Constant.of(element) + firstArrayField: string, + secondArray: Expr | any[], + ...otherArrays: Array +): FunctionExpr; + +export function arrayConcat( + firstArray: Expr | string, + secondArray: Expr | any[], + ...otherArrays: Array +): FunctionExpr { + const exprValues = otherArrays.map(element => valueToDefaultExpr(element)); + return fieldOfOrExpr(firstArray).arrayConcat( + fieldOfOrExpr(secondArray), + ...exprValues ); - return new ArrayConcat(arrayExpr, exprValues); } /** @@ -4171,14 +4331,14 @@ export function arrayConcat( * * ```typescript * // Check if the 'colors' array contains the value of field 'selectedColor' - * arrayContains(Field.of("colors"), Field.of("selectedColor")); + * arrayContains(field("colors"), field("selectedColor")); * ``` * * @param array The array expression to check. * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: Expr): ArrayContains; +export function arrayContains(array: Expr, element: Expr): FunctionExpr; /** * @beta @@ -4187,14 +4347,14 @@ export function arrayContains(array: Expr, element: Expr): ArrayContains; * * ```typescript * // Check if the 'colors' array contains "red" - * arrayContains(Field.of("colors"), "red"); + * arrayContains(field("colors"), "red"); * ``` * * @param array The array expression to check. * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: any): ArrayContains; +export function arrayContains(array: Expr, element: any): FunctionExpr; /** * @beta @@ -4203,14 +4363,14 @@ export function arrayContains(array: Expr, element: any): ArrayContains; * * ```typescript * // Check if the 'colors' array contains the value of field 'selectedColor' - * arrayContains("colors", Field.of("selectedColor")); + * arrayContains("colors", field("selectedColor")); * ``` * - * @param array The field name to check. + * @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(array: string, element: Expr): ArrayContains; +export function arrayContains(fieldName: string, element: Expr): FunctionExpr; /** * @beta @@ -4222,18 +4382,15 @@ export function arrayContains(array: string, element: Expr): ArrayContains; * arrayContains("colors", "red"); * ``` * - * @param array The field name to check. + * @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(array: string, element: any): ArrayContains; -export function arrayContains( - array: Expr | string, - element: any -): ArrayContains { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const elementExpr = element instanceof Expr ? element : Constant.of(element); - return new ArrayContains(arrayExpr, elementExpr); +export function arrayContains(fieldName: string, element: any): BooleanExpr; +export function arrayContains(array: Expr | string, element: any): BooleanExpr { + const arrayExpr = fieldOfOrExpr(array); + const elementExpr = valueToDefaultExpr(element); + return arrayExpr.arrayContains(elementExpr); } /** @@ -4244,52 +4401,55 @@ export function arrayContains( * * ```typescript * // Check if the 'categories' array contains either values from field "cate1" or "Science" - * arrayContainsAny(Field.of("categories"), [Field.of("cate1"), "Science"]); + * arrayContainsAny(field("categories"), [field("cate1"), "Science"]); * ``` * * @param array The array expression to check. * @param values The elements to check for in the array. * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ -export function arrayContainsAny(array: Expr, values: Expr[]): ArrayContainsAny; +export function arrayContainsAny( + array: Expr, + values: Array +): BooleanExpr; /** * @beta * - * Creates an expression that checks if an array expression contains any of the specified + * Creates an expression that checks if a field's array value contains any of the specified * elements. * * ```typescript - * // Check if the 'categories' array contains either values from field "cate1" or "Science" - * arrayContainsAny(Field.of("categories"), [Field.of("cate1"), "Science"]); + * // Check if the 'groups' array contains either the value from the 'userGroup' field + * // or the value "guest" + * arrayContainsAny("categories", [field("cate1"), "Science"]); * ``` * - * @param array The array expression to check. + * @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(array: Expr, values: any[]): ArrayContainsAny; +export function arrayContainsAny( + fieldName: string, + values: Array +): BooleanExpr; /** * @beta * - * Creates an expression that checks if a field's array value contains any of the specified + * Creates an expression that checks if an array expression 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.of("cate1"), "Science"]); + * // Check if the 'categories' array contains either values from field "cate1" or "Science" + * arrayContainsAny(field("categories"), array([field("cate1"), "Science"])); * ``` * - * @param array The field name to check. - * @param values The elements to check for in the array. + * @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: string, - values: Expr[] -): ArrayContainsAny; +export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; /** * @beta @@ -4300,26 +4460,20 @@ export function arrayContainsAny( * ```typescript * // Check if the 'groups' array contains either the value from the 'userGroup' field * // or the value "guest" - * arrayContainsAny("categories", [Field.of("cate1"), "Science"]); + * arrayContainsAny("categories", array([field("cate1"), "Science"])); * ``` * - * @param array The field name to check. - * @param values The elements to check for in the array. + * @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( - array: string, - values: any[] -): ArrayContainsAny; +export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; export function arrayContainsAny( array: Expr | string, - values: any[] -): ArrayContainsAny { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAny(arrayExpr, exprValues); + values: any[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOfOrExpr(array).arrayContainsAny(values); } /** @@ -4329,50 +4483,56 @@ export function arrayContainsAny( * * ```typescript * // Check if the "tags" array contains all of the values: "SciFi", "Adventure", and the value from field "tag1" - * arrayContainsAll(Field.of("tags"), [Field.of("tag1"), Constant.of("SciFi"), Constant.of("Adventure")]); + * arrayContainsAll(field("tags"), [field("tag1"), constant("SciFi"), "Adventure"]); * ``` * * @param array The array expression to check. * @param values The elements to check for in the array. * @return A new {@code Expr} representing the 'array_contains_all' comparison. */ -export function arrayContainsAll(array: Expr, values: Expr[]): ArrayContainsAll; +export function arrayContainsAll( + array: Expr, + values: Array +): BooleanExpr; /** * @beta * - * Creates an expression that checks if an array expression contains all the specified elements. + * 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 all of the values: "SciFi", "Adventure", and the value from field "tag1" - * arrayContainsAll(Field.of("tags"), [Field.of("tag1"), "SciFi", "Adventure"]); + * // 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 array The array expression to check. + * @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(array: Expr, values: any[]): ArrayContainsAll; +export function arrayContainsAll( + fieldName: string, + values: Array +): BooleanExpr; /** * @beta * - * Creates an expression that checks if a field's array value contains all the specified values or - * expressions. + * Creates an expression that checks if an array expression contains all the specified elements. * * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * arrayContainsAll("tags", [Field.of("tag1"), "SciFi", "Adventure"]); + * // 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 field name to check. - * @param values The elements to check for in the array. + * @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: string, - values: Expr[] -): ArrayContainsAll; + array: Expr, + arrayExpression: Expr +): BooleanExpr; /** * @beta @@ -4381,27 +4541,24 @@ export function arrayContainsAll( * expressions. * * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * arrayContainsAll("tags", [Field.of("tag1"), "SciFi", "Adventure"]); + * // 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 array The field name to check. - * @param values The elements to check for in the array. + * @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( - array: string, - values: any[] -): ArrayContainsAll; + fieldName: string, + arrayExpression: Expr +): BooleanExpr; export function arrayContainsAll( array: Expr | string, - values: any[] -): ArrayContainsAll { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAll(arrayExpr, exprValues); + values: any[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOfOrExpr(array).arrayContainsAll(values); } /** @@ -4411,49 +4568,48 @@ export function arrayContainsAll( * * ```typescript * // Get the number of items in the 'cart' array - * arrayLength(Field.of("cart")); + * arrayLength(field("cart")); * ``` * * @param array The array expression to calculate the length of. * @return A new {@code Expr} representing the length of the array. */ -export function arrayLength(array: Expr): ArrayLength { - return new ArrayLength(array); +export function arrayLength(array: Expr): FunctionExpr { + return array.arrayLength(); } /** * @beta * - * Creates an expression that checks if an expression is equal to any of the provided values or + * Creates an expression that checks if an expression, when evaluated, is equal to any of the provided values or * expressions. * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny(Field.of("category"), [Constant.of("Electronics"), Field.of("primaryType")]); + * eqAny(field("category"), [constant("Electronics"), field("primaryType")]); * ``` * - * @param element The expression to compare. - * @param others The values to check against. + * @param expression The expression whose results to compare. + * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(element: Expr, others: Expr[]): EqAny; +export function eqAny(expression: Expr, values: Array): BooleanExpr; /** * @beta * - * Creates an expression that checks if an expression is equal to any of the provided values or - * expressions. + * Creates an expression that checks if an expression is equal to any of the provided values. * * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny(Field.of("category"), ["Electronics", Field.of("primaryType")]); + * // Check if the 'category' field is set to a value in the disabledCategories field + * eqAny(field("category"), field('disabledCategories')); * ``` * - * @param element The expression to compare. - * @param others The values to check against. + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(element: Expr, others: any[]): EqAny; +export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; /** * @beta @@ -4463,14 +4619,17 @@ export function eqAny(element: Expr, others: any[]): EqAny; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", [Constant.of("Electronics"), Field.of("primaryType")]); + * eqAny("category", [constant("Electronics"), field("primaryType")]); * ``` * - * @param element The field to compare. - * @param others The values to check against. + * @param fieldName The field to compare. + * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(element: string, others: Expr[]): EqAny; +export function eqAny( + fieldName: string, + values: Array +): BooleanExpr; /** * @beta @@ -4480,20 +4639,20 @@ export function eqAny(element: string, others: Expr[]): EqAny; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", ["Electronics", Field.of("primaryType")]); + * eqAny("category", ["Electronics", field("primaryType")]); * ``` * - * @param element The field to compare. - * @param others The values to check against. + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input field. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(element: string, others: any[]): EqAny; -export function eqAny(element: Expr | string, others: any[]): EqAny { - const elementExpr = element instanceof Expr ? element : Field.of(element); - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new EqAny(elementExpr, exprOthers); +export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; +export function eqAny( + element: Expr | string, + values: any[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOfOrExpr(element).eqAny(values); } /** @@ -4504,78 +4663,80 @@ export function eqAny(element: Expr | string, others: any[]): EqAny { * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny(Field.of("status"), [Constant.of("pending"), Field.of("rejectedStatus")]); + * notEqAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * * @param element The expression to compare. - * @param others The values to check against. + * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: Expr, others: Expr[]): NotEqAny; +export function notEqAny(element: Expr, values: Array): BooleanExpr; /** * @beta * - * Creates an expression that checks if an expression is not equal to any of the provided values + * Creates an expression that checks if a field's value is not equal to any of the provided values * or expressions. * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny(Field.of("status"), ["pending", Field.of("rejectedStatus")]); + * notEqAny("status", [constant("pending"), field("rejectedStatus")]); * ``` * - * @param element The expression to compare. - * @param others The values to check against. + * @param fieldName The field name to compare. + * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: Expr, others: any[]): NotEqAny; +export function notEqAny( + fieldName: string, + values: Array +): BooleanExpr; /** * @beta * - * Creates an expression that checks if a field's value is not equal to any of the provided values + * Creates an expression that checks if an expression is not equal to any of the provided values * or expressions. * * ```typescript - * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny("status", [Constant.of("pending"), Field.of("rejectedStatus")]); + * // Check if the 'status' field is neither "pending" nor the value of the field 'rejectedStatus' + * notEqAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * - * @param element The field name to compare. - * @param others The values to check against. + * @param element The expression to compare. + * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: string, others: Expr[]): NotEqAny; +export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; /** * @beta * - * Creates an expression that checks if a field's value is not equal to any of the provided values - * or expressions. + * 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 neither "pending" nor the value of 'rejectedStatus' - * notEqAny("status", ["pending", Field.of("rejectedStatus")]); + * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' + * notEqAny("status", field("rejectedStatuses")); * ``` * - * @param element The field name to compare. - * @param others The values to check against. + * @param fieldName The field name to compare. + * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: string, others: any[]): NotEqAny; -export function notEqAny(element: Expr | string, others: any[]): NotEqAny { - const elementExpr = element instanceof Expr ? element : Field.of(element); - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new NotEqAny(elementExpr, exprOthers); +export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; + +export function notEqAny( + element: Expr | string, + values: any[] | Expr +): BooleanExpr { + // @ts-ignore implementation accepts both types + return fieldOfOrExpr(element).notEqAny(values); } /** * @beta * - * Creates an expression that performs a logical 'XOR' (exclusive OR) operation on multiple filter - * conditions. + * 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", @@ -4586,12 +4747,17 @@ export function notEqAny(element: Expr | string, others: any[]): NotEqAny { * eq("status", "active")); * ``` * - * @param left The first filter condition. - * @param right Additional filter conditions to 'XOR' together. + * @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(left: FilterCondition, ...right: FilterCondition[]): Xor { - return new Xor([left, ...right]); +export function xor( + first: BooleanExpr, + second: BooleanExpr, + ...additionalConditions: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('xor', [first, second, ...additionalConditions]); } /** @@ -4603,7 +4769,7 @@ export function xor(left: FilterCondition, ...right: FilterCondition[]): Xor { * ```typescript * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". * cond( - * gt("age", 18), Constant.of("Adult"), Constant.of("Minor")); + * gt("age", 18), constant("Adult"), constant("Minor")); * ``` * * @param condition The condition to evaluate. @@ -4612,11 +4778,11 @@ export function xor(left: FilterCondition, ...right: FilterCondition[]): Xor { * @return A new {@code Expr} representing the conditional expression. */ export function cond( - condition: FilterCondition, + condition: BooleanExpr, thenExpr: Expr, elseExpr: Expr -): Cond { - return new Cond(condition, thenExpr, elseExpr); +): FunctionExpr { + return new FunctionExpr('cond', [condition, thenExpr, elseExpr]); } /** @@ -4629,155 +4795,126 @@ export function cond( * not(eq("completed", true)); * ``` * - * @param filter The filter condition to negate. + * @param booleanExpr The filter condition to negate. * @return A new {@code Expr} representing the negated filter condition. */ -export function not(filter: FilterCondition): Not { - return new Not(filter); +export function not(booleanExpr: BooleanExpr): BooleanExpr { + return booleanExpr.not(); } /** * @beta * - * Creates an expression that returns the larger value between two expressions, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'field1' field and the 'field2' field. - * logicalMaximum(Field.of("field1"), Field.of("field2")); - * ``` - * - * @param left The left operand expression. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMaximum(left: Expr, right: Expr): LogicalMaximum; - -/** - * @beta - * - * Creates an expression that returns the larger value between an expression and a constant value, based on Firestore's value type ordering. + * 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 larger value between the 'value' field and 10. - * logicalMaximum(Field.of("value"), 10); + * // Returns the largest value between the 'field1' field, the 'field2' field, + * // and 1000 + * logicalMaximum(field("field1"), field("field2"), 1000); * ``` * - * @param left The left operand expression. - * @param right The right operand constant. + * @param first The first operand expression. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. * @return A new {@code Expr} representing the logical max operation. */ -export function logicalMaximum(left: Expr, right: any): LogicalMaximum; +export function logicalMaximum( + first: Expr, + second: Expr | any, + ...others: Array +): FunctionExpr; /** * @beta * - * Creates an expression that returns the larger value between a field and an expression, based on Firestore's value type ordering. + * 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 larger value between the 'field1' field and the 'field2' field. - * logicalMaximum("field1", Field.of('field2')); + * // Returns the largest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMaximum("field1", field("field2"), 1000); * ``` * - * @param left The left operand field name. - * @param right The right operand expression. + * @param fieldName The first operand field name. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. * @return A new {@code Expr} representing the logical max operation. */ -export function logicalMaximum(left: string, right: Expr): LogicalMaximum; +export function logicalMaximum( + fieldName: string, + second: Expr | any, + ...others: Array +): FunctionExpr; -/** - * @beta - * - * Creates an expression that returns the larger value between a field and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'value' field and 10. - * logicalMaximum("value", 10); - * ``` - * - * @param left The left operand field name. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMaximum(left: string, right: any): LogicalMaximum; export function logicalMaximum( - left: Expr | string, - right: Expr | any -): LogicalMaximum { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new LogicalMaximum(normalizedLeft, normalizedRight); + first: Expr | string, + second: Expr | any, + ...others: Array +): FunctionExpr { + return fieldOfOrExpr(first).logicalMaximum( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); } /** * @beta * - * Creates an expression that returns the smaller value between two expressions, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'field1' field and the 'field2' field. - * logicalMinimum(Field.of("field1"), Field.of("field2")); - * ``` - * - * @param left The left operand expression. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMinimum(left: Expr, right: Expr): LogicalMinimum; - -/** - * @beta - * - * Creates an expression that returns the smaller value between an expression and a constant value, based on Firestore's value type ordering. + * 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 smaller value between the 'value' field and 10. - * logicalMinimum(Field.of("value"), 10); + * // Returns the smallest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMinimum(field("field1"), field("field2"), 1000); * ``` * - * @param left The left operand expression. - * @param right The right operand constant. + * @param first The first operand expression. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. * @return A new {@code Expr} representing the logical min operation. */ -export function logicalMinimum(left: Expr, right: any): LogicalMinimum; +export function logicalMinimum( + first: Expr, + second: Expr | any, + ...others: Array +): FunctionExpr; /** * @beta * - * Creates an expression that returns the smaller value between a field and an expression, based on Firestore's value type ordering. + * 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 smaller value between the 'field1' field and the 'field2' field. - * logicalMinimum("field1", Field.of("field2")); + * // Returns the smallest value between the 'field1' field, the 'field2' field, + * // and 1000. + * logicalMinimum("field1", field("field2"), 1000); * ``` * - * @param left The left operand field name. - * @param right The right operand expression. + * @param fieldName The first operand field name. + * @param second The second expression or literal. + * @param others Optional additional expressions or literals. * @return A new {@code Expr} representing the logical min operation. */ -export function logicalMinimum(left: string, right: Expr): LogicalMinimum; +export function logicalMinimum( + fieldName: string, + second: Expr | any, + ...others: Array +): FunctionExpr; -/** - * @beta - * - * Creates an expression that returns the smaller value between a field and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'value' field and 10. - * logicalMinimum("value", 10); - * ``` - * - * @param left The left operand field name. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMinimum(left: string, right: any): LogicalMinimum; export function logicalMinimum( - left: Expr | string, - right: Expr | any -): LogicalMinimum { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new LogicalMinimum(normalizedLeft, normalizedRight); + first: Expr | string, + second: Expr | any, + ...others: Array +): FunctionExpr { + return fieldOfOrExpr(first).logicalMinimum( + valueToDefaultExpr(second), + ...others.map(value => valueToDefaultExpr(value)) + ); } /** @@ -4787,13 +4924,13 @@ export function logicalMinimum( * * ```typescript * // Check if the document has a field named "phoneNumber" - * exists(Field.of("phoneNumber")); + * exists(field("phoneNumber")); * ``` * * @param value An expression evaluates to the name of the field to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(value: Expr): Exists; +export function exists(value: Expr): BooleanExpr; /** * @beta @@ -4805,14 +4942,12 @@ export function exists(value: Expr): Exists; * exists("phoneNumber"); * ``` * - * @param field The field name to check. + * @param fieldName The field name to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(field: string): Exists; -export function exists(valueOrField: Expr | string): Exists { - const valueExpr = - valueOrField instanceof Expr ? valueOrField : Field.of(valueOrField); - return new Exists(valueExpr); +export function exists(fieldName: string): BooleanExpr; +export function exists(valueOrField: Expr | string): BooleanExpr { + return fieldOfOrExpr(valueOrField).exists(); } /** @@ -4822,13 +4957,13 @@ export function exists(valueOrField: Expr | string): Exists { * * ```typescript * // Check if the result of a calculation is NaN - * isNaN(Field.of("value").divide(0)); + * isNaN(field("value").divide(0)); * ``` * * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(value: Expr): IsNan; +export function isNan(value: Expr): BooleanExpr; /** * @beta @@ -4840,13 +4975,12 @@ export function isNan(value: Expr): IsNan; * isNaN("value"); * ``` * - * @param value The name of the field to check. + * @param fieldName The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(value: string): IsNan; -export function isNan(value: Expr | string): IsNan { - const valueExpr = value instanceof Expr ? value : Field.of(value); - return new IsNan(valueExpr); +export function isNan(fieldName: string): BooleanExpr; +export function isNan(value: Expr | string): BooleanExpr { + return fieldOfOrExpr(value).isNan(); } /** @@ -4856,18 +4990,18 @@ export function isNan(value: Expr | string): IsNan { * * ```typescript * // Reverse the value of the 'myString' field. - * reverse(Field.of("myString")); + * reverse(field("myString")); * ``` * - * @param expr The expression representing the string to reverse. + * @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(expr: Expr): Reverse; +export function reverse(stringExpression: Expr): FunctionExpr; /** * @beta * - * Creates an expression that reverses a string represented by a field. + * Creates an expression that reverses a string value in the specified field. * * ```typescript * // Reverse the value of the 'myString' field. @@ -4877,10 +5011,9 @@ export function reverse(expr: Expr): Reverse; * @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): Reverse; -export function reverse(expr: Expr | string): Reverse { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new Reverse(normalizedExpr); +export function reverse(field: string): FunctionExpr; +export function reverse(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).reverse(); } /** @@ -4890,7 +5023,7 @@ export function reverse(expr: Expr | string): Reverse { * * ```typescript * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst(Field.of("message"), "hello", "hi"); + * replaceFirst(field("message"), "hello", "hi"); * ``` * * @param value The expression representing the string to perform the replacement on. @@ -4902,7 +5035,7 @@ export function replaceFirst( value: Expr, find: string, replace: string -): ReplaceFirst; +): FunctionExpr; /** * @beta @@ -4912,7 +5045,7 @@ export function replaceFirst( * * ```typescript * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceFirst(Field.of("message"), Field.of("findField"), Field.of("replaceField")); + * replaceFirst(field("message"), field("findField"), field("replaceField")); * ``` * * @param value The expression representing the string to perform the replacement on. @@ -4924,7 +5057,7 @@ export function replaceFirst( value: Expr, find: Expr, replace: Expr -): ReplaceFirst; +): FunctionExpr; /** * @beta @@ -4936,26 +5069,25 @@ export function replaceFirst( * replaceFirst("message", "hello", "hi"); * ``` * - * @param field The name of the field representing the string to perform the replacement on. + * @param fieldName The name of the field representing the string to perform the replacement on. * @param find The substring to search for. * @param replace The substring to replace the first occurrence of 'find' with. * @return A new {@code Expr} representing the string with the first occurrence replaced. */ export function replaceFirst( - field: string, + fieldName: string, find: string, replace: string -): ReplaceFirst; +): FunctionExpr; export function replaceFirst( value: Expr | string, find: Expr | string, replace: Expr | string -): ReplaceFirst { - const normalizedValue = typeof value === 'string' ? Field.of(value) : value; - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceFirst(normalizedValue, normalizedFind, normalizedReplace); +): FunctionExpr { + const normalizedValue = fieldOfOrExpr(value); + const normalizedFind = valueToDefaultExpr(find); + const normalizedReplace = valueToDefaultExpr(replace); + return normalizedValue.replaceFirst(normalizedFind, normalizedReplace); } /** @@ -4965,7 +5097,7 @@ export function replaceFirst( * * ```typescript * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll(Field.of("message"), "hello", "hi"); + * replaceAll(field("message"), "hello", "hi"); * ``` * * @param value The expression representing the string to perform the replacement on. @@ -4977,7 +5109,7 @@ export function replaceAll( value: Expr, find: string, replace: string -): ReplaceAll; +): FunctionExpr; /** * @beta @@ -4987,7 +5119,7 @@ export function replaceAll( * * ```typescript * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceAll(Field.of("message"), Field.of("findField"), Field.of("replaceField")); + * replaceAll(field("message"), field("findField"), field("replaceField")); * ``` * * @param value The expression representing the string to perform the replacement on. @@ -4995,7 +5127,11 @@ export function replaceAll( * @param replace The expression representing the substring to replace all occurrences of 'find' with. * @return A new {@code Expr} representing the string with all occurrences replaced. */ -export function replaceAll(value: Expr, find: Expr, replace: Expr): ReplaceAll; +export function replaceAll( + value: Expr, + find: Expr, + replace: Expr +): FunctionExpr; /** * @beta @@ -5007,26 +5143,25 @@ export function replaceAll(value: Expr, find: Expr, replace: Expr): ReplaceAll; * replaceAll("message", "hello", "hi"); * ``` * - * @param field The name of the field representing the string to perform the replacement on. + * @param fieldName The name of the field representing the string to perform the replacement on. * @param find The substring to search for. * @param replace The substring to replace all occurrences of 'find' with. * @return A new {@code Expr} representing the string with all occurrences replaced. */ export function replaceAll( - field: string, + fieldName: string, find: string, replace: string -): ReplaceAll; +): FunctionExpr; export function replaceAll( value: Expr | string, find: Expr | string, replace: Expr | string -): ReplaceAll { - const normalizedValue = typeof value === 'string' ? Field.of(value) : value; - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceAll(normalizedValue, normalizedFind, normalizedReplace); +): FunctionExpr { + const normalizedValue = fieldOfOrExpr(value); + const normalizedFind = valueToDefaultExpr(find); + const normalizedReplace = valueToDefaultExpr(replace); + return normalizedValue.replaceAll(normalizedFind, normalizedReplace); } /** @@ -5036,13 +5171,13 @@ export function replaceAll( * * ```typescript * // Calculate the length of the 'myString' field in bytes. - * byteLength(Field.of("myString")); + * byteLength(field("myString")); * ``` * * @param expr The expression representing the string. * @return A new {@code Expr} representing the length of the string in bytes. */ -export function byteLength(expr: Expr): ByteLength; +export function byteLength(expr: Expr): FunctionExpr; /** * @beta @@ -5054,13 +5189,13 @@ export function byteLength(expr: Expr): ByteLength; * byteLength("myString"); * ``` * - * @param field The name of the field representing the string. + * @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(field: string): ByteLength; -export function byteLength(expr: Expr | string): ByteLength { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new ByteLength(normalizedExpr); +export function byteLength(fieldName: string): FunctionExpr; +export function byteLength(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOfOrExpr(expr); + return normalizedExpr.byteLength(); } /** @@ -5073,10 +5208,10 @@ export function byteLength(expr: Expr | string): ByteLength { * strLength("name"); * ``` * - * @param field The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the length of the string. */ -export function charLength(field: string): CharLength; +export function charLength(fieldName: string): FunctionExpr; /** * @beta @@ -5085,16 +5220,16 @@ export function charLength(field: string): CharLength; * * ```typescript * // Get the character length of the 'name' field in UTF-8. - * strLength(Field.of("name")); + * strLength(field("name")); * ``` * - * @param expr The expression representing the string to calculate the length of. + * @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(expr: Expr): CharLength; -export function charLength(value: Expr | string): CharLength { - const valueExpr = value instanceof Expr ? value : Field.of(value); - return new CharLength(valueExpr); +export function charLength(stringExpression: Expr): FunctionExpr; +export function charLength(value: Expr | string): FunctionExpr { + const valueExpr = fieldOfOrExpr(value); + return valueExpr.charLength(); } /** @@ -5108,11 +5243,11 @@ export function charLength(value: Expr | string): CharLength { * like("title", "%guide%"); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: string): Like; +export function like(fieldName: string, pattern: string): BooleanExpr; /** * @beta @@ -5122,14 +5257,14 @@ export function like(left: string, pattern: string): Like; * * ```typescript * // Check if the 'title' field contains the string "guide" - * like("title", Field.of("pattern")); + * like("title", field("pattern")); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: Expr): Like; +export function like(fieldName: string, pattern: Expr): BooleanExpr; /** * @beta @@ -5138,14 +5273,14 @@ export function like(left: string, pattern: Expr): Like; * * ```typescript * // Check if the 'title' field contains the string "guide" - * like(Field.of("title"), "%guide%"); + * like(field("title"), "%guide%"); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @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(left: Expr, pattern: string): Like; +export function like(stringExpression: Expr, pattern: string): BooleanExpr; /** * @beta @@ -5154,18 +5289,21 @@ export function like(left: Expr, pattern: string): Like; * * ```typescript * // Check if the 'title' field contains the string "guide" - * like(Field.of("title"), Field.of("pattern")); + * like(field("title"), field("pattern")); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @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(left: Expr, pattern: Expr): Like; -export function like(left: Expr | string, pattern: Expr | string): Like { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new Like(leftExpr, patternExpr); +export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; +export function like( + left: Expr | string, + pattern: Expr | string +): FunctionExpr { + const leftExpr = fieldOfOrExpr(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.like(patternExpr); } /** @@ -5179,11 +5317,11 @@ export function like(left: Expr | string, pattern: Expr | string): Like { * regexContains("description", "(?i)example"); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: string): RegexContains; +export function regexContains(fieldName: string, pattern: string): BooleanExpr; /** * @beta @@ -5193,14 +5331,14 @@ export function regexContains(left: string, pattern: string): RegexContains; * * ```typescript * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains("description", Field.of("pattern")); + * regexContains("description", field("pattern")); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: Expr): RegexContains; +export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; /** * @beta @@ -5210,14 +5348,17 @@ export function regexContains(left: string, pattern: Expr): RegexContains; * * ```typescript * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains(Field.of("description"), "(?i)example"); + * regexContains(field("description"), "(?i)example"); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @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(left: Expr, pattern: string): RegexContains; +export function regexContains( + stringExpression: Expr, + pattern: string +): BooleanExpr; /** * @beta @@ -5227,21 +5368,24 @@ export function regexContains(left: Expr, pattern: string): RegexContains; * * ```typescript * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains(Field.of("description"), Field.of("pattern")); + * regexContains(field("description"), field("pattern")); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @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(left: Expr, pattern: Expr): RegexContains; +export function regexContains( + stringExpression: Expr, + pattern: Expr +): BooleanExpr; export function regexContains( left: Expr | string, pattern: Expr | string -): RegexContains { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new RegexContains(leftExpr, patternExpr); +): BooleanExpr { + const leftExpr = fieldOfOrExpr(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.regexContains(patternExpr); } /** @@ -5254,11 +5398,11 @@ export function regexContains( * regexMatch("email", "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: string): RegexMatch; +export function regexMatch(fieldName: string, pattern: string): BooleanExpr; /** * @beta @@ -5267,14 +5411,14 @@ export function regexMatch(left: string, pattern: string): RegexMatch; * * ```typescript * // Check if the 'email' field matches a valid email pattern - * regexMatch("email", Field.of("pattern")); + * regexMatch("email", field("pattern")); * ``` * - * @param left The name of the field containing the string. + * @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(left: string, pattern: Expr): RegexMatch; +export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; /** * @beta @@ -5284,14 +5428,17 @@ export function regexMatch(left: string, pattern: Expr): RegexMatch; * * ```typescript * // Check if the 'email' field matches a valid email pattern - * regexMatch(Field.of("email"), "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); + * regexMatch(field("email"), "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); * ``` * - * @param left The expression representing the string to match against. + * @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(left: Expr, pattern: string): RegexMatch; +export function regexMatch( + stringExpression: Expr, + pattern: string +): BooleanExpr; /** * @beta @@ -5301,21 +5448,21 @@ export function regexMatch(left: Expr, pattern: string): RegexMatch; * * ```typescript * // Check if the 'email' field matches a valid email pattern - * regexMatch(Field.of("email"), Field.of("pattern")); + * regexMatch(field("email"), field("pattern")); * ``` * - * @param left The expression representing the string to match against. + * @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(left: Expr, pattern: Expr): RegexMatch; +export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; export function regexMatch( left: Expr | string, pattern: Expr | string -): RegexMatch { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new RegexMatch(leftExpr, patternExpr); +): BooleanExpr { + const leftExpr = fieldOfOrExpr(left); + const patternExpr = valueToDefaultExpr(pattern); + return leftExpr.regexMatch(patternExpr); } /** @@ -5328,11 +5475,11 @@ export function regexMatch( * strContains("description", "example"); * ``` * - * @param left The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(left: string, substring: string): StrContains; +export function strContains(fieldName: string, substring: string): BooleanExpr; /** * @beta @@ -5341,14 +5488,14 @@ export function strContains(left: string, substring: string): StrContains; * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains("description", Field.of("keyword")); + * strContains("description", field("keyword")); * ``` * - * @param left The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @param substring The expression representing the substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(left: string, substring: Expr): StrContains; +export function strContains(fieldName: string, substring: Expr): BooleanExpr; /** * @beta @@ -5357,14 +5504,17 @@ export function strContains(left: string, substring: Expr): StrContains; * * ```typescript * // Check if the 'description' field contains "example". - * strContains(Field.of("description"), "example"); + * strContains(field("description"), "example"); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @param stringExpression The expression representing the string to perform the comparison on. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(left: Expr, substring: string): StrContains; +export function strContains( + stringExpression: Expr, + substring: string +): BooleanExpr; /** * @beta @@ -5373,22 +5523,24 @@ export function strContains(left: Expr, substring: string): StrContains; * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains(Field.of("description"), Field.of("keyword")); + * strContains(field("description"), field("keyword")); * ``` * - * @param left The expression representing the string to perform the comparison on. + * @param stringExpression The expression representing the string to perform the comparison on. * @param substring The expression representing the substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(left: Expr, substring: Expr): StrContains; +export function strContains( + stringExpression: Expr, + substring: Expr +): BooleanExpr; export function strContains( left: Expr | string, substring: Expr | string -): StrContains { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const substringExpr = - substring instanceof Expr ? substring : Constant.of(substring); - return new StrContains(leftExpr, substringExpr); +): BooleanExpr { + const leftExpr = fieldOfOrExpr(left); + const substringExpr = valueToDefaultExpr(substring); + return leftExpr.strContains(substringExpr); } /** @@ -5401,11 +5553,11 @@ export function strContains( * startsWith("name", "Mr."); * ``` * - * @param expr The field name to check. + * @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(expr: string, prefix: string): StartsWith; +export function startsWith(fieldName: string, prefix: string): BooleanExpr; /** * @beta @@ -5414,14 +5566,14 @@ export function startsWith(expr: string, prefix: string): StartsWith; * * ```typescript * // Check if the 'fullName' field starts with the value of the 'firstName' field - * startsWith("fullName", Field.of("firstName")); + * startsWith("fullName", field("firstName")); * ``` * - * @param expr The field name to check. + * @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(expr: string, prefix: Expr): StartsWith; +export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; /** * @beta @@ -5430,14 +5582,14 @@ export function startsWith(expr: string, prefix: Expr): StartsWith; * * ```typescript * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." - * startsWith(Field.of("fullName"), "Mr."); + * startsWith(field("fullName"), "Mr."); * ``` * - * @param expr The expression to check. + * @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(expr: Expr, prefix: string): StartsWith; +export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; /** * @beta @@ -5446,21 +5598,19 @@ export function startsWith(expr: Expr, prefix: string): StartsWith; * * ```typescript * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." - * startsWith(Field.of("fullName"), Field.of("prefix")); + * startsWith(field("fullName"), field("prefix")); * ``` * - * @param expr The expression to check. + * @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(expr: Expr, prefix: Expr): StartsWith; +export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; export function startsWith( expr: Expr | string, prefix: Expr | string -): StartsWith { - const exprLeft = expr instanceof Expr ? expr : Field.of(expr); - const prefixExpr = prefix instanceof Expr ? prefix : Constant.of(prefix); - return new StartsWith(exprLeft, prefixExpr); +): BooleanExpr { + return fieldOfOrExpr(expr).startsWith(valueToDefaultExpr(prefix)); } /** @@ -5473,11 +5623,11 @@ export function startsWith( * endsWith("filename", ".txt"); * ``` * - * @param expr The field name to check. + * @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(expr: string, suffix: string): EndsWith; +export function endsWith(fieldName: string, suffix: string): BooleanExpr; /** * @beta @@ -5486,14 +5636,14 @@ export function endsWith(expr: string, suffix: string): EndsWith; * * ```typescript * // Check if the 'url' field ends with the value of the 'extension' field - * endsWith("url", Field.of("extension")); + * endsWith("url", field("extension")); * ``` * - * @param expr The field name to check. + * @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(expr: string, suffix: Expr): EndsWith; +export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; /** * @beta @@ -5502,14 +5652,14 @@ export function endsWith(expr: string, suffix: Expr): EndsWith; * * ```typescript * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." - * endsWith(Field.of("fullName"), "Jr."); + * endsWith(field("fullName"), "Jr."); * ``` * - * @param expr The expression to check. + * @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(expr: Expr, suffix: string): EndsWith; +export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; /** * @beta @@ -5518,18 +5668,19 @@ export function endsWith(expr: Expr, suffix: string): EndsWith; * * ```typescript * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." - * endsWith(Field.of("fullName"), Constant.of("Jr.")); + * endsWith(field("fullName"), constant("Jr.")); * ``` * - * @param expr The expression to check. + * @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(expr: Expr, suffix: Expr): EndsWith; -export function endsWith(expr: Expr | string, suffix: Expr | string): EndsWith { - const exprLeft = expr instanceof Expr ? expr : Field.of(expr); - const suffixExpr = suffix instanceof Expr ? suffix : Constant.of(suffix); - return new EndsWith(exprLeft, suffixExpr); +export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; +export function endsWith( + expr: Expr | string, + suffix: Expr | string +): BooleanExpr { + return fieldOfOrExpr(expr).endsWith(valueToDefaultExpr(suffix)); } /** @@ -5542,10 +5693,10 @@ export function endsWith(expr: Expr | string, suffix: Expr | string): EndsWith { * toLower("name"); * ``` * - * @param expr The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(expr: string): ToLower; +export function toLower(fieldName: string): FunctionExpr; /** * @beta @@ -5554,15 +5705,15 @@ export function toLower(expr: string): ToLower; * * ```typescript * // Convert the 'name' field to lowercase - * toLower(Field.of("name")); + * toLower(field("name")); * ``` * - * @param expr The expression representing the string to convert to lowercase. + * @param stringExpression The expression representing the string to convert to lowercase. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(expr: Expr): ToLower; -export function toLower(expr: Expr | string): ToLower { - return new ToLower(expr instanceof Expr ? expr : Field.of(expr)); +export function toLower(stringExpression: Expr): FunctionExpr; +export function toLower(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).toLower(); } /** @@ -5575,10 +5726,10 @@ export function toLower(expr: Expr | string): ToLower { * toUpper("title"); * ``` * - * @param expr The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(expr: string): ToUpper; +export function toUpper(fieldName: string): FunctionExpr; /** * @beta @@ -5587,15 +5738,15 @@ export function toUpper(expr: string): ToUpper; * * ```typescript * // Convert the 'title' field to uppercase - * toUppercase(Field.of("title")); + * toUppercase(field("title")); * ``` * - * @param expr The expression representing the string to convert to uppercase. + * @param stringExpression The expression representing the string to convert to uppercase. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(expr: Expr): ToUpper; -export function toUpper(expr: Expr | string): ToUpper { - return new ToUpper(expr instanceof Expr ? expr : Field.of(expr)); +export function toUpper(stringExpression: Expr): FunctionExpr; +export function toUpper(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).toUpper(); } /** @@ -5608,10 +5759,10 @@ export function toUpper(expr: Expr | string): ToUpper { * trim("userInput"); * ``` * - * @param expr The name of the field containing the string. + * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(expr: string): Trim; +export function trim(fieldName: string): FunctionExpr; /** * @beta @@ -5620,15 +5771,15 @@ export function trim(expr: string): Trim; * * ```typescript * // Trim whitespace from the 'userInput' field - * trim(Field.of("userInput")); + * trim(field("userInput")); * ``` * - * @param expr The expression representing the string to trim. + * @param stringExpression The expression representing the string to trim. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(expr: Expr): Trim; -export function trim(expr: Expr | string): Trim { - return new Trim(expr instanceof Expr ? expr : Field.of(expr)); +export function trim(stringExpression: Expr): FunctionExpr; +export function trim(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).trim(); } /** @@ -5638,17 +5789,19 @@ export function trim(expr: Expr | string): Trim { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat("firstName", " ", Field.of("lastName")); + * strConcat("firstName", " ", field("lastName")); * ``` * - * @param first The field name containing the initial string value. - * @param elements The expressions (typically strings) to concatenate. + * @param fieldName The field name containing the initial string value. + * @param secondString An expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ export function strConcat( - first: string, - ...elements: Array -): StrConcat; + fieldName: string, + secondString: Expr | string, + ...otherStrings: Array +): FunctionExpr; /** * @beta @@ -5656,23 +5809,28 @@ export function strConcat( * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat(Field.of("firstName"), " ", Field.of("lastName")); + * strConcat(field("firstName"), " ", field("lastName")); * ``` * - * @param first The initial string expression to concatenate to. - * @param elements The expressions (typically strings) to concatenate. + * @param firstString The initial string expression to concatenate to. + * @param secondString An expression or string literal to concatenate. + * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ export function strConcat( - first: Expr, - ...elements: Array -): StrConcat; + firstString: Expr, + secondString: Expr | string, + ...otherStrings: Array +): FunctionExpr; export function strConcat( first: string | Expr, + second: string | Expr, ...elements: Array -): StrConcat { - const exprs = elements.map(e => (e instanceof Expr ? e : Constant.of(e))); - return new StrConcat(first instanceof Expr ? first : Field.of(first), exprs); +): FunctionExpr { + return valueToDefaultExpr(first).strConcat( + valueToDefaultExpr(second), + ...elements.map(valueToDefaultExpr) + ); } /** @@ -5685,11 +5843,11 @@ export function strConcat( * mapGet("address", "city"); * ``` * - * @param mapField The field name of the map field. + * @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(mapField: string, subField: string): MapGet; +export function mapGet(fieldName: string, subField: string): FunctionExpr; /** * @beta @@ -5698,19 +5856,19 @@ export function mapGet(mapField: string, subField: string): MapGet; * * ```typescript * // Get the 'city' value from the 'address' map field - * mapGet(Field.of("address"), "city"); + * mapGet(field("address"), "city"); * ``` * - * @param mapExpr The expression representing the map. + * @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(mapExpr: Expr, subField: string): MapGet; -export function mapGet(fieldOrExpr: string | Expr, subField: string): MapGet { - return new MapGet( - typeof fieldOrExpr === 'string' ? Field.of(fieldOrExpr) : fieldOrExpr, - subField - ); +export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; +export function mapGet( + fieldOrExpr: string | Expr, + subField: string +): FunctionExpr { + return fieldOfOrExpr(fieldOrExpr).mapGet(subField); } /** @@ -5719,14 +5877,14 @@ export function mapGet(fieldOrExpr: string | Expr, subField: string): MapGet { * Creates an aggregation that counts the total number of stage inputs. * * ```typescript - * // Count the total number of users - * countAll().as("totalUsers"); + * // Count the total number of input documents + * countAll().as("totalDocument"); * ``` * - * @return A new {@code Accumulator} representing the 'countAll' aggregation. + * @return A new {@code AggregateFunction} representing the 'countAll' aggregation. */ -export function countAll(): Count { - return new Count(undefined, false); +export function countAll(): AggregateFunction { + return new AggregateFunction('count', []); } /** @@ -5737,13 +5895,13 @@ export function countAll(): Count { * * ```typescript * // Count the number of items where the price is greater than 10 - * count(Field.of("price").gt(10)).as("expensiveItemCount"); + * count(field("price").gt(10)).as("expensiveItemCount"); * ``` * - * @param value The expression to count. - * @return A new {@code Accumulator} representing the 'count' aggregation. + * @param expression The expression to count. + * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ -export function countFunction(value: Expr): Count; +export function countFunction(expression: Expr): AggregateFunction; /** * Creates an aggregation that counts the number of stage inputs with valid evaluations of the @@ -5754,13 +5912,12 @@ export function countFunction(value: Expr): Count; * count("productId").as("totalProducts"); * ``` * - * @param value The name of the field to count. - * @return A new {@code Accumulator} representing the 'count' aggregation. + * @param fieldName The name of the field to count. + * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ -export function countFunction(value: string): Count; -export function countFunction(value: Expr | string): Count { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Count(exprValue, false); +export function countFunction(fieldName: string): AggregateFunction; +export function countFunction(value: Expr | string): AggregateFunction { + return fieldOfOrExpr(value).count(); } /** @@ -5771,13 +5928,13 @@ export function countFunction(value: Expr | string): Count { * * ```typescript * // Calculate the total revenue from a set of orders - * sum(Field.of("orderAmount")).as("totalRevenue"); + * sum(field("orderAmount")).as("totalRevenue"); * ``` * - * @param value The expression to sum up. - * @return A new {@code Accumulator} representing the 'sum' aggregation. + * @param expression The expression to sum up. + * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ -export function sumFunction(value: Expr): Sum; +export function sumFunction(expression: Expr): AggregateFunction; /** * @beta @@ -5790,13 +5947,12 @@ export function sumFunction(value: Expr): Sum; * sum("orderAmount").as("totalRevenue"); * ``` * - * @param value The name of the field containing numeric values to sum up. - * @return A new {@code Accumulator} representing the 'sum' aggregation. + * @param fieldName The name of the field containing numeric values to sum up. + * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ -export function sumFunction(value: string): Sum; -export function sumFunction(value: Expr | string): Sum { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Sum(exprValue, false); +export function sumFunction(fieldName: string): AggregateFunction; +export function sumFunction(value: Expr | string): AggregateFunction { + return fieldOfOrExpr(value).sum(); } /** @@ -5807,13 +5963,13 @@ export function sumFunction(value: Expr | string): Sum { * * ```typescript * // Calculate the average age of users - * avg(Field.of("age")).as("averageAge"); + * avg(field("age")).as("averageAge"); * ``` * - * @param value The expression representing the values to average. - * @return A new {@code Accumulator} representing the 'avg' aggregation. + * @param expression The expression representing the values to average. + * @return A new {@code AggregateFunction} representing the 'avg' aggregation. */ -export function avgFunction(value: Expr): Avg; +export function avgFunction(expression: Expr): AggregateFunction; /** * @beta @@ -5826,13 +5982,12 @@ export function avgFunction(value: Expr): Avg; * avg("age").as("averageAge"); * ``` * - * @param value The name of the field containing numeric values to average. - * @return A new {@code Accumulator} representing the 'avg' aggregation. + * @param fieldName The name of the field containing numeric values to average. + * @return A new {@code AggregateFunction} representing the 'avg' aggregation. */ -export function avgFunction(value: string): Avg; -export function avgFunction(value: Expr | string): Avg { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Avg(exprValue, false); +export function avgFunction(fieldName: string): AggregateFunction; +export function avgFunction(value: Expr | string): AggregateFunction { + return fieldOfOrExpr(value).avg(); } /** @@ -5843,13 +5998,13 @@ export function avgFunction(value: Expr | string): Avg { * * ```typescript * // Find the lowest price of all products - * minimum(Field.of("price")).as("lowestPrice"); + * minimum(field("price")).as("lowestPrice"); * ``` * - * @param value The expression to find the minimum value of. - * @return A new {@code Accumulator} representing the 'min' aggregation. + * @param expression The expression to find the minimum value of. + * @return A new {@code AggregateFunction} representing the 'min' aggregation. */ -export function minimum(value: Expr): Minimum; +export function minimum(expression: Expr): AggregateFunction; /** * @beta @@ -5861,13 +6016,12 @@ export function minimum(value: Expr): Minimum; * minimum("price").as("lowestPrice"); * ``` * - * @param value The name of the field to find the minimum value of. - * @return A new {@code Accumulator} representing the 'min' aggregation. + * @param fieldName The name of the field to find the minimum value of. + * @return A new {@code AggregateFunction} representing the 'min' aggregation. */ -export function minimum(value: string): Minimum; -export function minimum(value: Expr | string): Minimum { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Minimum(exprValue, false); +export function minimum(fieldName: string): AggregateFunction; +export function minimum(value: Expr | string): AggregateFunction { + return fieldOfOrExpr(value).minimum(); } /** @@ -5878,13 +6032,13 @@ export function minimum(value: Expr | string): Minimum { * * ```typescript * // Find the highest score in a leaderboard - * maximum(Field.of("score")).as("highestScore"); + * maximum(field("score")).as("highestScore"); * ``` * - * @param value The expression to find the maximum value of. - * @return A new {@code Accumulator} representing the 'max' aggregation. + * @param expression The expression to find the maximum value of. + * @return A new {@code AggregateFunction} representing the 'max' aggregation. */ -export function maximum(value: Expr): Maximum; +export function maximum(expression: Expr): AggregateFunction; /** * @beta @@ -5896,49 +6050,32 @@ export function maximum(value: Expr): Maximum; * maximum("score").as("highestScore"); * ``` * - * @param value The name of the field to find the maximum value of. - * @return A new {@code Accumulator} representing the 'max' aggregation. + * @param fieldName The name of the field to find the maximum value of. + * @return A new {@code AggregateFunction} representing the 'max' aggregation. */ -export function maximum(value: string): Maximum; -export function maximum(value: Expr | string): Maximum { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Maximum(exprValue, false); +export function maximum(fieldName: string): AggregateFunction; +export function maximum(value: Expr | string): AggregateFunction { + return fieldOfOrExpr(value).maximum(); } /** * @beta * - * Calculates the Cosine distance between a field's vector value and a double array. + * 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 expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the Cosine distance between the two vectors. - */ -export function cosineDistance(expr: string, other: number[]): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a field's vector value and a VectorValue. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * cosineDistance("location", new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to compare against. + * @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( - expr: string, - other: VectorValue -): CosineDistance; + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta @@ -5947,46 +6084,36 @@ export function cosineDistance( * * ```typescript * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * cosineDistance("userVector", Field.of("itemVector")); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other 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(expr: string, other: Expr): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a vector expression and a double array. - * - * ```typescript - * // Calculate the cosine distance between the 'location' field and a target location - * cosineDistance(Field.of("location"), [37.7749, -122.4194]); + * cosineDistance("userVector", field("itemVector")); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as an array of doubles) to compare against. + * @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(expr: Expr, other: number[]): CosineDistance; +export function cosineDistance( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; /** * @beta * - * Calculates the Cosine distance between a vector expression and a VectorValue. + * 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.of("location"), new VectorValue([37.7749, -122.4194])); + * cosineDistance(field("location"), [37.7749, -122.4194]); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as a VectorValue) to compare against. + * @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(expr: Expr, other: VectorValue): CosineDistance; +export function cosineDistance( + vectorExpression: Expr, + vector: number[] | Expr +): FunctionExpr; /** * @beta @@ -5995,21 +6122,24 @@ export function cosineDistance(expr: Expr, other: VectorValue): CosineDistance; * * ```typescript * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * cosineDistance(Field.of("userVector"), Field.of("itemVector")); + * cosineDistance(field("userVector"), field("itemVector")); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (represented as an Expr) to compare against. + * @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(expr: Expr, other: Expr): CosineDistance; +export function cosineDistance( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; export function cosineDistance( expr: Expr | string, other: Expr | number[] | VectorValue -): CosineDistance { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new CosineDistance(expr1, expr2); +): FunctionExpr { + const expr1 = fieldOfOrExpr(expr); + const expr2 = vectorToExpr(other); + return expr1.cosineDistance(expr2); } /** @@ -6022,215 +6152,248 @@ export function cosineDistance( * dotProduct("features", [0.5, 0.8, 0.2]); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to calculate with. + * @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(expr: string, other: number[]): DotProduct; +export function dotProduct( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the dot product between a field's vector value and a VectorValue. + * Calculates the dot product between a field's vector value and a vector expression. * * ```typescript - * // Calculate the dot product distance between a feature vector and a target vector - * dotProduct("features", new VectorValue([0.5, 0.8, 0.2])); + * // Calculate the dot product distance between two document vectors: 'docVector1' and 'docVector2' + * dotProduct("docVector1", field("docVector2")); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to calculate with. + * @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(expr: string, other: VectorValue): DotProduct; +export function dotProduct( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; /** * @beta * - * Calculates the dot product between a field's vector value and a vector expression. + * Calculates the dot product between a vector expression and a double array. * * ```typescript - * // Calculate the dot product distance between two document vectors: 'docVector1' and 'docVector2' - * dotProduct("docVector1", Field.of("docVector2")); + * // Calculate the dot product between a feature vector and a target vector + * dotProduct(field("features"), [0.5, 0.8, 0.2]); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (represented as an Expr) to calculate with. + * @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(expr: string, other: Expr): DotProduct; +export function dotProduct( + vectorExpression: Expr, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the dot product between a vector expression and a double array. + * Calculates the dot product between two vector expressions. * * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * dotProduct(Field.of("features"), [0.5, 0.8, 0.2]); + * // Calculate the dot product between two document vectors: 'docVector1' and 'docVector2' + * dotProduct(field("docVector1"), field("docVector2")); * ``` * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (as an array of doubles) to calculate with. + * @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(expr: Expr, other: number[]): DotProduct; +export function dotProduct( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function dotProduct( + expr: Expr | string, + other: Expr | number[] | VectorValue +): FunctionExpr { + const expr1 = fieldOfOrExpr(expr); + const expr2 = vectorToExpr(other); + return expr1.dotProduct(expr2); +} /** * @beta * - * Calculates the dot product between a vector expression and a VectorValue. + * Calculates the Euclidean distance between a field's vector value and a double array. * * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * dotProduct(Field.of("features"), new VectorValue([0.5, 0.8, 0.2])); + * // Calculate the Euclidean distance between the 'location' field and a target location + * euclideanDistance("location", [37.7749, -122.4194]); * ``` * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (as a VectorValue) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. + * @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 dotProduct(expr: Expr, other: VectorValue): DotProduct; +export function euclideanDistance( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the dot product between two vector expressions. + * Calculates the Euclidean distance between a field's vector value and a vector expression. * * ```typescript - * // Calculate the dot product between two document vectors: 'docVector1' and 'docVector2' - * dotProduct(Field.of("docVector1"), Field.of("docVector2")); + * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' + * euclideanDistance("pointA", field("pointB")); * ``` * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (represented as an Expr) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. + * @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 dotProduct(expr: Expr, other: Expr): DotProduct; -export function dotProduct( - expr: Expr | string, - other: Expr | number[] | VectorValue -): DotProduct { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new DotProduct(expr1, expr2); -} +export function euclideanDistance( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; /** * @beta * - * Calculates the Euclidean distance between a field's vector value and a double array. + * 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("location", [37.7749, -122.4194]); + * + * euclideanDistance(field("location"), [37.7749, -122.4194]); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to compare against. + * @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( - expr: string, - other: number[] -): EuclideanDistance; + vectorExpression: Expr, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the Euclidean distance between a field's vector value and a VectorValue. + * Calculates the Euclidean distance between two vector expressions. * * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * euclideanDistance("location", new VectorValue([37.7749, -122.4194])); + * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' + * euclideanDistance(field("pointA"), field("pointB")); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to compare against. + * @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( - expr: string, - other: VectorValue -): EuclideanDistance; + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function euclideanDistance( + expr: Expr | string, + other: Expr | number[] | VectorValue +): FunctionExpr { + const expr1 = fieldOfOrExpr(expr); + const expr2 = vectorToExpr(other); + return expr1.euclideanDistance(expr2); +} /** * @beta * - * Calculates the Euclidean distance between a field's vector value and a vector expression. + * Calculates the Manhattan distance between a field's vector value and a double array. * * ```typescript - * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' - * euclideanDistance("pointA", Field.of("pointB")); + * // Calculate the Manhattan distance between the 'location' field and a target location + * manhattanDistance("location", [37.7749, -122.4194]); * ``` * - * @param expr The name of the field containing the first vector. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + * @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 Manhattan distance between the two vectors. */ -export function euclideanDistance(expr: string, other: Expr): EuclideanDistance; +export function manhattanDistance( + fieldName: string, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the Euclidean distance between a vector expression and a double array. + * Calculates the Manhattan distance between a field's vector value and a vector expression. * * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * - * euclideanDistance(Field.of("location"), [37.7749, -122.4194]); + * // Calculate the Manhattan distance between two vector fields: 'pointA' and 'pointB' + * manhattanDistance("pointA", field("pointB")); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + * @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 Manhattan distance between the two vectors. */ -export function euclideanDistance( - expr: Expr, - other: number[] -): EuclideanDistance; +export function manhattanDistance( + fieldName: string, + vectorExpression: Expr +): FunctionExpr; /** * @beta * - * Calculates the Euclidean distance between a vector expression and a VectorValue. + * Calculates the Manhattan distance between a vector expression and a double array. * * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * euclideanDistance(Field.of("location"), new VectorValue([37.7749, -122.4194])); + * // Calculate the Manhattan distance between the 'location' field and a target location + * + * manhattanDistance(field("location"), [37.7749, -122.4194]); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as a VectorValue) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + * @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 Manhattan distance between the two vectors. */ -export function euclideanDistance( - expr: Expr, - other: VectorValue -): EuclideanDistance; +export function manhattanDistance( + vectorExpression: Expr, + vector: number[] | VectorValue +): FunctionExpr; /** * @beta * - * Calculates the Euclidean distance between two vector expressions. + * Calculates the Manhattan distance between two vector expressions. * * ```typescript - * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' - * euclideanDistance(Field.of("pointA"), Field.of("pointB")); + * // Calculate the Manhattan distance between two vector fields: 'pointA' and 'pointB' + * manhattanDistance(field("pointA"), field("pointB")); * ``` * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. + * @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 Manhattan distance between the two vectors. */ -export function euclideanDistance(expr: Expr, other: Expr): EuclideanDistance; -export function euclideanDistance( - expr: Expr | string, +export function manhattanDistance( + vectorExpression: Expr, + otherVectorExpression: Expr +): FunctionExpr; +export function manhattanDistance( + fieldOrExpr: Expr | string, other: Expr | number[] | VectorValue -): EuclideanDistance { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new EuclideanDistance(expr1, expr2); +): FunctionExpr { + const expr1 = fieldOfOrExpr(fieldOrExpr); + const expr2 = vectorToExpr(other); + return expr1.manhattanDistance(expr2); } /** @@ -6240,13 +6403,13 @@ export function euclideanDistance( * * ```typescript * // Get the vector length (dimension) of the field 'embedding'. - * vectorLength(Field.of("embedding")); + * vectorLength(field("embedding")); * ``` * - * @param expr The expression representing the Firestore Vector. + * @param vectorExpression The expression representing the Firestore Vector. * @return A new {@code Expr} representing the length of the array. */ -export function vectorLength(expr: Expr): VectorLength; +export function vectorLength(vectorExpression: Expr): FunctionExpr; /** * @beta @@ -6258,13 +6421,12 @@ export function vectorLength(expr: Expr): VectorLength; * vectorLength("embedding"); * ``` * - * @param field The name of the field representing the Firestore Vector. + * @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(field: string): VectorLength; -export function vectorLength(expr: Expr | string): VectorLength { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new VectorLength(normalizedExpr); +export function vectorLength(fieldName: string): FunctionExpr; +export function vectorLength(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).vectorLength(); } /** @@ -6275,13 +6437,13 @@ export function vectorLength(expr: Expr | string): VectorLength { * * ```typescript * // Interpret the 'microseconds' field as microseconds since epoch. - * unixMicrosToTimestamp(Field.of("microseconds")); + * unixMicrosToTimestamp(field("microseconds")); * ``` * * @param expr The expression representing the number of microseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMicrosToTimestamp(expr: Expr): UnixMicrosToTimestamp; +export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; /** * @beta @@ -6294,15 +6456,12 @@ export function unixMicrosToTimestamp(expr: Expr): UnixMicrosToTimestamp; * unixMicrosToTimestamp("microseconds"); * ``` * - * @param field The name of the field representing the number of microseconds since epoch. + * @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(field: string): UnixMicrosToTimestamp; -export function unixMicrosToTimestamp( - expr: Expr | string -): UnixMicrosToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixMicrosToTimestamp(normalizedExpr); +export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; +export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).unixMicrosToTimestamp(); } /** @@ -6312,13 +6471,13 @@ export function unixMicrosToTimestamp( * * ```typescript * // Convert the 'timestamp' field to microseconds since epoch. - * timestampToUnixMicros(Field.of("timestamp")); + * timestampToUnixMicros(field("timestamp")); * ``` * * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of microseconds since epoch. */ -export function timestampToUnixMicros(expr: Expr): TimestampToUnixMicros; +export function timestampToUnixMicros(expr: Expr): FunctionExpr; /** * @beta @@ -6330,15 +6489,12 @@ export function timestampToUnixMicros(expr: Expr): TimestampToUnixMicros; * timestampToUnixMicros("timestamp"); * ``` * - * @param field The name of the field representing the 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(field: string): TimestampToUnixMicros; -export function timestampToUnixMicros( - expr: Expr | string -): TimestampToUnixMicros { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixMicros(normalizedExpr); +export function timestampToUnixMicros(fieldName: string): FunctionExpr; +export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { + return fieldOfOrExpr(expr).timestampToUnixMicros(); } /** @@ -6349,13 +6505,13 @@ export function timestampToUnixMicros( * * ```typescript * // Interpret the 'milliseconds' field as milliseconds since epoch. - * unixMillisToTimestamp(Field.of("milliseconds")); + * unixMillisToTimestamp(field("milliseconds")); * ``` * * @param expr The expression representing the number of milliseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMillisToTimestamp(expr: Expr): UnixMillisToTimestamp; +export function unixMillisToTimestamp(expr: Expr): FunctionExpr; /** * @beta @@ -6368,15 +6524,13 @@ export function unixMillisToTimestamp(expr: Expr): UnixMillisToTimestamp; * unixMillisToTimestamp("milliseconds"); * ``` * - * @param field The name of the field representing the number of milliseconds since epoch. + * @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(field: string): UnixMillisToTimestamp; -export function unixMillisToTimestamp( - expr: Expr | string -): UnixMillisToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixMillisToTimestamp(normalizedExpr); +export function unixMillisToTimestamp(fieldName: string): FunctionExpr; +export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOfOrExpr(expr); + return normalizedExpr.unixMillisToTimestamp(); } /** @@ -6386,13 +6540,13 @@ export function unixMillisToTimestamp( * * ```typescript * // Convert the 'timestamp' field to milliseconds since epoch. - * timestampToUnixMillis(Field.of("timestamp")); + * timestampToUnixMillis(field("timestamp")); * ``` * * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of milliseconds since epoch. */ -export function timestampToUnixMillis(expr: Expr): TimestampToUnixMillis; +export function timestampToUnixMillis(expr: Expr): FunctionExpr; /** * @beta @@ -6404,15 +6558,13 @@ export function timestampToUnixMillis(expr: Expr): TimestampToUnixMillis; * timestampToUnixMillis("timestamp"); * ``` * - * @param field The name of the field representing the 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(field: string): TimestampToUnixMillis; -export function timestampToUnixMillis( - expr: Expr | string -): TimestampToUnixMillis { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixMillis(normalizedExpr); +export function timestampToUnixMillis(fieldName: string): FunctionExpr; +export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOfOrExpr(expr); + return normalizedExpr.timestampToUnixMillis(); } /** @@ -6423,13 +6575,13 @@ export function timestampToUnixMillis( * * ```typescript * // Interpret the 'seconds' field as seconds since epoch. - * unixSecondsToTimestamp(Field.of("seconds")); + * unixSecondsToTimestamp(field("seconds")); * ``` * * @param expr The expression representing the number of seconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixSecondsToTimestamp(expr: Expr): UnixSecondsToTimestamp; +export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; /** * @beta @@ -6442,15 +6594,13 @@ export function unixSecondsToTimestamp(expr: Expr): UnixSecondsToTimestamp; * unixSecondsToTimestamp("seconds"); * ``` * - * @param field The name of the field representing the number of seconds since epoch. + * @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(field: string): UnixSecondsToTimestamp; -export function unixSecondsToTimestamp( - expr: Expr | string -): UnixSecondsToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixSecondsToTimestamp(normalizedExpr); +export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; +export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOfOrExpr(expr); + return normalizedExpr.unixSecondsToTimestamp(); } /** @@ -6460,13 +6610,13 @@ export function unixSecondsToTimestamp( * * ```typescript * // Convert the 'timestamp' field to seconds since epoch. - * timestampToUnixSeconds(Field.of("timestamp")); + * timestampToUnixSeconds(field("timestamp")); * ``` * * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of seconds since epoch. */ -export function timestampToUnixSeconds(expr: Expr): TimestampToUnixSeconds; +export function timestampToUnixSeconds(expr: Expr): FunctionExpr; /** * @beta @@ -6478,15 +6628,13 @@ export function timestampToUnixSeconds(expr: Expr): TimestampToUnixSeconds; * timestampToUnixSeconds("timestamp"); * ``` * - * @param field The name of the field representing the 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(field: string): TimestampToUnixSeconds; -export function timestampToUnixSeconds( - expr: Expr | string -): TimestampToUnixSeconds { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixSeconds(normalizedExpr); +export function timestampToUnixSeconds(fieldName: string): FunctionExpr; +export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { + const normalizedExpr = fieldOfOrExpr(expr); + return normalizedExpr.timestampToUnixSeconds(); } /** @@ -6496,7 +6644,7 @@ export function timestampToUnixSeconds( * * ```typescript * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. - * timestampAdd(Field.of("timestamp"), Field.of("unit"), Field.of("amount")); + * timestampAdd(field("timestamp"), field("unit"), field("amount")); * ``` * * @param timestamp The expression representing the timestamp. @@ -6508,7 +6656,7 @@ export function timestampAdd( timestamp: Expr, unit: Expr, amount: Expr -): TimestampAdd; +): FunctionExpr; /** * @beta @@ -6517,7 +6665,7 @@ export function timestampAdd( * * ```typescript * // Add 1 day to the 'timestamp' field. - * timestampAdd(Field.of("timestamp"), "day", 1); + * timestampAdd(field("timestamp"), "day", 1); * ``` * * @param timestamp The expression representing the timestamp. @@ -6529,7 +6677,7 @@ export function timestampAdd( timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): TimestampAdd; +): FunctionExpr; /** * @beta @@ -6541,16 +6689,16 @@ export function timestampAdd( * timestampAdd("timestamp", "day", 1); * ``` * - * @param field The name of the field representing the timestamp. + * @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( - field: string, + fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): TimestampAdd; +): FunctionExpr; export function timestampAdd( timestamp: Expr | string, unit: @@ -6562,17 +6710,11 @@ export function timestampAdd( | 'hour' | 'day', amount: Expr | number -): TimestampAdd { - const normalizedTimestamp = - typeof timestamp === 'string' ? Field.of(timestamp) : timestamp; - const normalizedUnit = unit instanceof Expr ? unit : Constant.of(unit); - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampAdd( - normalizedTimestamp, - normalizedUnit, - normalizedAmount - ); +): FunctionExpr { + const normalizedTimestamp = fieldOfOrExpr(timestamp); + const normalizedUnit = valueToDefaultExpr(unit); + const normalizedAmount = valueToDefaultExpr(amount); + return normalizedTimestamp.timestampAdd(normalizedUnit, normalizedAmount); } /** @@ -6582,7 +6724,7 @@ export function timestampAdd( * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * timestampSub(Field.of("timestamp"), Field.of("unit"), Field.of("amount")); + * timestampSub(field("timestamp"), field("unit"), field("amount")); * ``` * * @param timestamp The expression representing the timestamp. @@ -6594,7 +6736,7 @@ export function timestampSub( timestamp: Expr, unit: Expr, amount: Expr -): TimestampSub; +): FunctionExpr; /** * @beta @@ -6603,7 +6745,7 @@ export function timestampSub( * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * timestampSub(Field.of("timestamp"), "day", 1); + * timestampSub(field("timestamp"), "day", 1); * ``` * * @param timestamp The expression representing the timestamp. @@ -6615,7 +6757,7 @@ export function timestampSub( timestamp: Expr, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): TimestampSub; +): FunctionExpr; /** * @beta @@ -6627,16 +6769,16 @@ export function timestampSub( * timestampSub("timestamp", "day", 1); * ``` * - * @param field The name of the field representing the timestamp. + * @param fieldName The name of the field representing the timestamp. * @param unit The unit of time to subtract (e.g., "day", "hour"). * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ export function timestampSub( - field: string, + fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): TimestampSub; +): FunctionExpr; export function timestampSub( timestamp: Expr | string, unit: @@ -6648,39 +6790,11 @@ export function timestampSub( | 'hour' | 'day', amount: Expr | number -): TimestampSub { - const normalizedTimestamp = - typeof timestamp === 'string' ? Field.of(timestamp) : timestamp; - const normalizedUnit = unit instanceof Expr ? unit : Constant.of(unit); - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampSub( - normalizedTimestamp, - normalizedUnit, - normalizedAmount - ); -} - -/** - * @beta - * - * Creates functions that work on the backend but do not exist in the SDK yet. - * - * ```typescript - * // Call a user defined function named "myFunc" with the arguments 10 and 20 - * // This is the same of the 'sum(Field.of("price"))', if it did not exist - * genericFunction("sum", [Field.of("price")]); - * ``` - * - * @param name The name of the user defined function. - * @param params The arguments to pass to the function. - * @return A new {@code Function} representing the function call. - */ -export function genericFunction( - name: string, - params: Expr[] -): FirestoreFunction { - return new FirestoreFunction(name, params); +): FunctionExpr { + const normalizedTimestamp = fieldOfOrExpr(timestamp); + const normalizedUnit = valueToDefaultExpr(unit); + const normalizedAmount = valueToDefaultExpr(amount); + return normalizedTimestamp.timestampSub(normalizedUnit, normalizedAmount); } /** @@ -6694,15 +6808,17 @@ export function genericFunction( * const condition = and(gt("age", 18), eq("city", "London"), eq("status", "active")); * ``` * - * @param left The first filter condition. - * @param right Additional filter conditions to 'AND' together. + * @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 andFunction( - left: FilterCondition, - ...right: FilterCondition[] -): And { - return new And([left, ...right]); + first: BooleanExpr, + second: BooleanExpr, + ...more: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('and', [first, second, ...more]); } /** @@ -6716,51 +6832,87 @@ export function andFunction( * const condition = or(gt("age", 18), eq("city", "London"), eq("status", "active")); * ``` * - * @param left The first filter condition. - * @param right Additional filter conditions to 'OR' together. + * @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 orFunction( - left: FilterCondition, - ...right: FilterCondition[] -): Or { - return new Or([left, ...right]); + first: BooleanExpr, + second: BooleanExpr, + ...more: BooleanExpr[] +): BooleanExpr { + return new BooleanExpr('or', [first, second, ...more]); } /** * @beta * - * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. + * Creates an {@link Ordering} that sorts documents in ascending order based on an expression. * * ```typescript - * // Sort documents by the 'name' field in ascending order + * // Sort documents by the 'name' field in lowercase in ascending order * firestore.pipeline().collection("users") - * .sort(ascending(Field.of("name"))); + * .sort(ascending(field("name").toLower())); * ``` * * @param expr The expression to create an ascending ordering for. * @return A new `Ordering` for ascending sorting. */ -export function ascending(expr: Expr): Ordering { - return new Ordering(expr, 'ascending'); +export function ascending(expr: Expr): Ordering; + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in ascending order based on a field. + * + * ```typescript + * // Sort documents by the 'name' field in ascending order + * firestore.pipeline().collection("users") + * .sort(ascending("name")); + * ``` + * + * @param fieldName The field to create an ascending ordering for. + * @return A new `Ordering` for ascending sorting. + */ +export function ascending(fieldName: string): Ordering; +export function ascending(field: Expr | string): Ordering { + return new Ordering(fieldOfOrExpr(field), 'ascending'); } /** * @beta * - * Creates an {@link Ordering} that sorts documents in descending order based on this expression. + * Creates an {@link Ordering} that sorts documents in descending order based on an expression. * * ```typescript - * // Sort documents by the 'createdAt' field in descending order + * // Sort documents by the 'name' field in lowercase in descending order * firestore.pipeline().collection("users") - * .sort(descending(Field.of("createdAt"))); + * .sort(descending(field("name").toLower())); * ``` * * @param expr The expression to create a descending ordering for. * @return A new `Ordering` for descending sorting. */ -export function descending(expr: Expr): Ordering { - return new Ordering(expr, 'descending'); +export function descending(expr: Expr): Ordering; + +/** + * @beta + * + * Creates an {@link Ordering} that sorts documents in descending order based on a field. + * + * ```typescript + * // Sort documents by the 'name' field in descending order + * firestore.pipeline().collection("users") + * .sort(descending("name")); + * ``` + * + * @param fieldName The field to create a descending ordering for. + * @return A new `Ordering` for descending sorting. + */ +export function descending(fieldName: string): Ordering; +export function descending(field: Expr | string): Ordering { + return new Ordering(fieldOfOrExpr(field), 'descending'); } /** @@ -6770,7 +6922,7 @@ export function descending(expr: Expr): Ordering { * * You create `Ordering` instances using the `ascending` and `descending` helper functions. */ -export class Ordering { +export class Ordering implements ProtoValueSerializable, UserData { constructor( readonly expr: Expr, readonly direction: 'ascending' | 'descending' @@ -6798,4 +6950,6 @@ export class Ordering { _readUserData(dataReader: UserDataReader): void { this.expr._readUserData(dataReader); } + + _protoValueType: 'ProtoValue' = 'ProtoValue'; } diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts index dc0a6412481..27c41de1908 100644 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -18,12 +18,58 @@ import { ObjectValue } from '../model/object_value'; import { isOptionalEqual } from '../util/misc'; +import { Field } from './expressions'; import { FieldPath } from './field_path'; +import { Pipeline } from './pipeline'; import { DocumentData, DocumentReference, refEqual } from './reference'; import { fieldPathFromArgument } from './snapshot'; import { Timestamp } from './timestamp'; import { AbstractUserDataWriter } from './user_data_writer'; +export class PipelineSnapshot { + private readonly _pipeline: Pipeline; + private readonly _executionTime: Timestamp | undefined; + private readonly _results: PipelineResult[]; + constructor( + pipeline: Pipeline, + results: PipelineResult[], + executionTime?: Timestamp + ) { + this._pipeline = pipeline; + this._executionTime = executionTime; + this._results = results; + } + + /** + * The Pipeline on which you called `execute()` in order to get this + * `PipelineSnapshot`. + */ + get pipeline(): Pipeline { + return this._pipeline; + } + + /** An array of all the results in the `PipelineSnapshot`. */ + get results(): PipelineResult[] { + return this._results; + } + + /** + * The time at which the pipeline producing this result is executed. + * + * @type {Timestamp} + * @readonly + * + */ + get executionTime(): Timestamp { + if (this._executionTime === undefined) { + throw new Error( + "'executionTime' is expected to exist, but it is undefined" + ); + } + return this._executionTime; + } +} + /** * @beta * @@ -36,7 +82,6 @@ import { AbstractUserDataWriter } from './user_data_writer'; export class PipelineResult { private readonly _userDataWriter: AbstractUserDataWriter; - private readonly _executionTime: Timestamp | undefined; private readonly _createTime: Timestamp | undefined; private readonly _updateTime: Timestamp | undefined; @@ -69,13 +114,11 @@ export class PipelineResult { userDataWriter: AbstractUserDataWriter, ref?: DocumentReference, fields?: ObjectValue, - executionTime?: Timestamp, createTime?: Timestamp, updateTime?: Timestamp ) { this._ref = ref; this._userDataWriter = userDataWriter; - this._executionTime = executionTime; this._createTime = createTime; this._updateTime = updateTime; this._fields = fields; @@ -120,22 +163,6 @@ export class PipelineResult { return this._updateTime; } - /** - * 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; - } - /** * Retrieves all fields in the result as an object. Returns 'undefined' if * the document doesn't exist. @@ -166,7 +193,7 @@ export class PipelineResult { /** * Retrieves the field specified by `field`. * - * @param {string|FieldPath} field The field path + * @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. @@ -184,7 +211,7 @@ export class PipelineResult { // 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): any { + get(fieldPath: string | FieldPath | Field): any { if (this._fields === undefined) { return undefined; } diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index 856096037f8..421fc759bfb 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -15,7 +15,12 @@ * limitations under the License. */ -import { DocumentReference } from './reference'; +import { DatabaseId } from '../core/database_info'; +import { toPipeline } from '../core/pipeline-util'; +import { FirestoreError, Code } from '../util/error'; + +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference, Query } from './reference'; import { CollectionGroupSource, CollectionSource, @@ -35,6 +40,7 @@ export class PipelineSource { * @param _createPipeline */ constructor( + private databaseId: DatabaseId, /** * @internal * @private @@ -42,19 +48,89 @@ export class PipelineSource { public _createPipeline: (stages: Stage[]) => PipelineType ) {} - collection(collectionPath: string): PipelineType { - return this._createPipeline([new CollectionSource(collectionPath)]); + /** + * Set the pipeline's source to the collection specified by the given path. + * + * @param collectionPath A path to a collection that will be the source of this pipeline. + */ + collection(collectionPath: string): PipelineType; + + /** + * Set the pipeline's source to the collection specified by the given CollectionReference. + * + * @param collectionReference A CollectionReference for a collection that will be the source of this pipeline. + * The converter for this CollectionReference will be ignored and not have an effect on this pipeline. + * + * @throws {@FirestoreError} Thrown if the provided CollectionReference targets a different project or database than the pipeline. + */ + collection(collectionReference: CollectionReference): PipelineType; + collection(collection: CollectionReference | string): PipelineType { + if (collection instanceof CollectionReference) { + this._validateReference(collection); + return this._createPipeline([new CollectionSource(collection.path)]); + } else { + return this._createPipeline([new CollectionSource(collection)]); + } } + /** + * Set the pipeline's source to the collection group with the given id. + * + * @param collectionid The id of a collection group that will be the source of this pipeline. + */ collectionGroup(collectionId: string): PipelineType { return this._createPipeline([new CollectionGroupSource(collectionId)]); } + /** + * Set the pipeline's source to be all documents in this database. + */ database(): PipelineType { return this._createPipeline([new DatabaseSource()]); } - documents(docs: DocumentReference[]): PipelineType { + /** + * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. + * + * @param docs An array of paths and DocumentReferences specifying the individual documents that will be the source of this pipeline. + * The converters for these DocumentReferences will be ignored and not have an effect on this pipeline. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + documents(docs: Array): PipelineType { + docs.forEach(doc => { + if (doc instanceof DocumentReference) { + this._validateReference(doc); + } + }); + return this._createPipeline([DocumentsSource.of(docs)]); } + + /** + * Convert the given Query into an equivalent Pipeline. + * + * @param query A Query to be converted into a Pipeline. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + createFrom(query: Query): Pipeline { + return toPipeline(query._query, query.firestore); + } + + _validateReference(reference: CollectionReference | DocumentReference): void { + const refDbId = reference.firestore._databaseId; + if (!refDbId.isEqual(this.databaseId)) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Invalid ${ + reference instanceof CollectionReference + ? 'CollectionReference' + : 'DocumentReference' + }. ` + + `The project ID ("${refDbId.projectId}") or the database ("${refDbId.database}") does not match ` + + `the project ID ("${this.databaseId.projectId}") and database ("${this.databaseId.database}") of the target database of this Pipeline.` + ); + } + } } diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 2145952c004..a655ab153b8 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -19,32 +19,26 @@ import { ObjectValue } from '../model/object_value'; import { - ExecutePipelineRequest, - StructuredPipeline, + Pipeline as ProtoPipeline, Stage as ProtoStage } from '../protos/firestore_proto_api'; -import { invokeExecutePipeline } from '../remote/datastore'; -import { - getEncodedDatabaseId, - JsonProtoSerializer, - ProtoSerializable -} from '../remote/serializer'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; +import { isPlainObject } from '../util/input_validation'; -import { getDatastore } from './components'; import { Firestore } from './database'; import { - Accumulator, - AccumulatorTarget, + _mapValue, + AggregateFunction, + AggregateWithAlias, Expr, ExprWithAlias, Field, - Fields, - FilterCondition, + BooleanExpr, Ordering, - Selectable + Selectable, + field, + constant } from './expressions'; -import { PipelineResult } from './pipeline-result'; -import { DocumentReference } from './reference'; import { AddFields, Aggregate, @@ -54,8 +48,13 @@ import { GenericStage, Limit, Offset, + RemoveFields, + Replace, Select, Sort, + Sample, + Union, + Unnest, Stage, Where } from './stage'; @@ -97,30 +96,27 @@ function isReadableUserData(value: any): value is ReadableUserData { * const db: Firestore; // Assumes a valid firestore instance. * * // Example 1: Select specific fields and rename 'rating' to 'bookRating' - * const results1 = await db.pipeline() + * const results1 = await execute(db.pipeline() * .collection("books") - * .select("title", "author", Field.of("rating").as("bookRating")) - * .execute(); + * .select("title", "author", field("rating").as("bookRating"))); * * // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950 - * const results2 = await db.pipeline() + * const results2 = await execute(db.pipeline() * .collection("books") - * .where(and(Field.of("genre").eq("Science Fiction"), Field.of("published").gt(1950))) - * .execute(); + * .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 db.pipeline() + * const results3 = await execute(db.pipeline() * .collection("books") - * .where(Field.of("published").gt(1980)) - * .aggregate(avg(Field.of("rating")).as("averageRating")) - * .execute(); + * .where(field("published").gt(1980)) + * .aggregate(avg(field("rating")).as("averageRating"))); * ``` */ /** * Base-class implementation */ -export class Pipeline implements ProtoSerializable { +export class Pipeline implements ProtoSerializable { /** * @internal * @private @@ -162,27 +158,53 @@ export class Pipeline implements ProtoSerializable { * ```typescript * firestore.pipeline().collection("books") * .addFields( - * Field.of("rating").as("bookRating"), // Rename 'rating' to 'bookRating' - * add(5, Field.of("quantity")).as("totalCost") // Calculate 'totalCost' + * field("rating").as("bookRating"), // Rename 'rating' to 'bookRating' + * add(5, field("quantity")).as("totalCost") // Calculate 'totalCost' * ); * ``` * - * @param fields The fields to add to the documents, specified as {@link Selectable}s. + * @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(...fields: Selectable[]): Pipeline { - const copy = this.stages.map(s => s); - copy.push( + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline { + return this._addStage( new AddFields( - this.readUserData('addFields', this.selectablesToMap(fields)) + this.readUserData( + 'addFields', + this.selectablesToMap([field, ...additionalFields]) + ) ) ); - return this.newPipeline( - this._db, - this.userDataReader, - this._userDataWriter, - copy + } + + /** + * Remove fields from outputs of previous stages. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection('books') + * // removes field 'rating' and 'cost' from the previous stage outputs. + * .removeFields( + * field('rating'), + * 'cost' + * ); + * ``` + * + * @param fieldValue The first field to remove. + * @param additionalFields Optional additional fields to remove. + * @return A new Pipeline object with this stage appended to the stage list. + */ + removeFields( + fieldValue: Field | string, + ...additionalFields: Array + ): Pipeline { + const fieldExpressions = [fieldValue, ...additionalFields].map(f => + typeof f === 'string' ? field(f) : (f as Field) ); + this.readUserData('removeFields', fieldExpressions); + return this._addStage(new RemoveFields(fieldExpressions)); } /** @@ -198,7 +220,7 @@ export class Pipeline implements ProtoSerializable { * * *

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

Example: @@ -207,101 +229,36 @@ export class Pipeline implements ProtoSerializable { * firestore.pipeline().collection("books") * .select( * "firstName", - * Field.of("lastName"), - * Field.of("address").toUppercase().as("upperAddress"), + * field("lastName"), + * field("address").toUppercase().as("upperAddress"), * ); * ``` * - * @param selections The fields to include in the output documents, specified as {@link + * @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(...selections: Array): Pipeline { - const copy = this.stages.map(s => s); - let projections: Map = this.selectablesToMap(selections); - projections = this.readUserData('select', projections); - copy.push(new Select(projections)); - return this.newPipeline( - this._db, - this.userDataReader, - this._userDataWriter, - copy - ); - } - - private selectablesToMap( - selectables: Array - ): Map { - const result = new Map(); - for (const selectable of selectables) { - if (typeof selectable === 'string') { - result.set(selectable as string, Field.of(selectable)); - } else if (selectable instanceof Field) { - result.set((selectable as Field).fieldName(), selectable); - } else if (selectable instanceof Fields) { - const fields = selectable as Fields; - for (const field of fields.fieldList()) { - result.set(field.fieldName(), field); - } - } else if (selectable instanceof ExprWithAlias) { - const expr = selectable as ExprWithAlias; - result.set(expr.alias, expr.expr); - } - } - return result; - } - - /** - * Reads user data for each expression in the expressionMap. - * @param name Name of the calling function. Used for error messages when invalid user data is encountered. - * @param expressionMap - * @return the expressionMap argument. - * @private - */ - private readUserData< - T extends - | Map - | ReadableUserData[] - | ReadableUserData - >(name: string, expressionMap: T): T { - if (isReadableUserData(expressionMap)) { - expressionMap._readUserData(this.userDataReader); - } else if (Array.isArray(expressionMap)) { - expressionMap.forEach(readableData => - readableData._readUserData(this.userDataReader) - ); - } else { - expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); - } - return expressionMap; - } - - /** - * @internal - * @private - * @param db - * @param userDataReader - * @param userDataWriter - * @param stages - * @protected - */ - protected newPipeline( - db: Firestore, - userDataReader: UserDataReader, - userDataWriter: AbstractUserDataWriter, - stages: Stage[], - converter: unknown = {} + select( + selection: Selectable | string, + ...additionalSelections: Array ): Pipeline { - return new Pipeline(db, userDataReader, userDataWriter, stages); + let projections: Map = this.selectablesToMap([ + selection, + ...additionalSelections + ]); + projections = this.readUserData('select', projections); + return this._addStage(new Select(projections)); } /** * Filters the documents from previous stages to only include those matching the specified {@link - * FilterCondition}. + * BooleanExpr}. * *

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

* @@ -505,38 +450,38 @@ export class Pipeline implements ProtoSerializable { * // Calculate the average rating for each genre. * firestore.pipeline().collection("books") * .aggregate({ - * accumulators: [avg(Field.of("rating")).as("avg_rating")] + * accumulators: [avg(field("rating")).as("avg_rating")] * groups: ["genre"] * }); * ``` * - * @param aggregate An {@link Aggregate} object that specifies the grouping fields (if any) and - * the aggregation operations to perform. - * @return A new {@code Pipeline} object with this stage appended to the stage list. + * @param options An object that specifies the accumulators + * and optional grouping fields to perform. + * @return A new {@code Pipeline} object with this stage appended to the stage + * list. */ aggregate(options: { - accumulators: AccumulatorTarget[]; + accumulators: AggregateWithAlias[]; groups?: Array; }): Pipeline; aggregate( optionsOrTarget: - | AccumulatorTarget + | AggregateWithAlias | { - accumulators: AccumulatorTarget[]; + accumulators: AggregateWithAlias[]; groups?: Array; }, - ...rest: AccumulatorTarget[] + ...rest: AggregateWithAlias[] ): Pipeline { - const copy = this.stages.map(s => s); if ('accumulators' in optionsOrTarget) { - copy.push( + return this._addStage( new Aggregate( - new Map( - optionsOrTarget.accumulators.map((target: AccumulatorTarget) => [ - (target as unknown as AccumulatorTarget).alias, + new Map( + optionsOrTarget.accumulators.map((target: AggregateWithAlias) => [ + (target as unknown as AggregateWithAlias).alias, this.readUserData( 'aggregate', - (target as unknown as AccumulatorTarget).expr + (target as unknown as AggregateWithAlias).aggregate ) ]) ), @@ -547,14 +492,14 @@ export class Pipeline implements ProtoSerializable { ) ); } else { - copy.push( + return this._addStage( new Aggregate( - new Map( + new Map( [optionsOrTarget, ...rest].map(target => [ - (target as unknown as AccumulatorTarget).alias, + (target as unknown as AggregateWithAlias).alias, this.readUserData( 'aggregate', - (target as unknown as AccumulatorTarget).expr + (target as unknown as AggregateWithAlias).aggregate ) ]) ), @@ -562,23 +507,16 @@ export class Pipeline implements ProtoSerializable { ) ); } - return this.newPipeline( - this._db, - this.userDataReader, - this._userDataWriter, - copy - ); } findNearest(options: FindNearestOptions): Pipeline { - const copy = this.stages.map(s => s); const parseContext = this.userDataReader.createContext( UserDataSource.Argument, 'findNearest' ); const value = parseVectorValue(options.vectorValue, parseContext); const vectorObjectValue = new ObjectValue(value); - copy.push( + return this._addStage( new FindNearest( options.field, vectorObjectValue, @@ -587,12 +525,6 @@ export class Pipeline implements ProtoSerializable { options.distanceField ) ); - return this.newPipeline( - this._db, - this.userDataReader, - this._userDataWriter, - copy - ); } /** @@ -611,15 +543,16 @@ export class Pipeline implements ProtoSerializable { * // with the same rating * firestore.pipeline().collection("books") * .sort( - * Ordering.of(Field.of("rating")).descending(), - * Ordering.of(Field.of("title")) // Ascending order is the default + * Ordering.of(field("rating")).descending(), + * Ordering.of(field("title")) // Ascending order is the default * ); * ``` * - * @param orders One or more {@link Ordering} instances specifying the sorting criteria. + * @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(...orderings: Ordering[]): Pipeline; + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; sort( optionsOrOrderings: | Ordering @@ -628,10 +561,9 @@ export class Pipeline implements ProtoSerializable { }, ...rest: Ordering[] ): Pipeline { - const copy = this.stages.map(s => s); // Option object - if ('orderings' in optionsOrOrderings) { - copy.push( + if (optionsOrOrderings && 'orderings' in optionsOrOrderings) { + return this._addStage( new Sort( this.readUserData( 'sort', @@ -641,17 +573,168 @@ export class Pipeline implements ProtoSerializable { ); } else { // Ordering object - copy.push( + return this._addStage( new Sort(this.readUserData('sort', [optionsOrOrderings, ...rest])) ); } + } - return this.newPipeline( - this._db, - this.userDataReader, - this._userDataWriter, - copy - ); + /** + * 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(field('parents')); + * + * // Output + * // { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * ``` + * + * @param field The {@link Field} field containing the nested map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(fieldValue: Field | string): Pipeline { + const fieldExpr = + typeof fieldValue === 'string' ? field(fieldValue) : fieldValue; + return this._addStage(new Replace(fieldExpr, 'full_replace')); + } + + /** + * Performs a pseudo-random sampling of the documents from the previous stage. + * + *

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

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

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

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

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

Example: + * + * ```typescript + * // Emit documents from books collection and magazines collection. + * firestore.pipeline().collection('books') + * .union(firestore.pipeline().collection('magazines')); + * ``` + * + * @param other The other {@code Pipeline} that is part of union. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + union(other: Pipeline): Pipeline { + return this._addStage(new Union(other)); + } + + /** + * Produces a document for each element in array found in previous stage document. + * + * For each previous stage document, this stage will emit zero or more augmented documents. The + * input array found in the previous stage document field 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 unnested 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 unnested element is from + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + unnest(selectable: Selectable, indexField?: string): Pipeline { + this.readUserData('unnest', selectable.expr); + + const alias = field(selectable.alias); + this.readUserData('unnest', alias); + + return this._addStage(new Unnest(selectable.expr, alias, indexField)); } /** @@ -666,7 +749,7 @@ export class Pipeline implements ProtoSerializable { * ```typescript * // Assume we don't have a built-in "where" stage * firestore.pipeline().collection("books") - * .genericStage("where", [Field.of("published").lt(1900)]) // Custom "where" stage + * .genericStage("where", [field("published").lt(1900)]) // Custom "where" stage * .select("title", "author"); * ``` * @@ -675,13 +758,45 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ genericStage(name: string, params: any[]): Pipeline { - const copy = this.stages.map(s => s); - params.forEach(param => { + // Convert input values to Expressions. + // We treat objects as mapValues and arrays as arrayValues, + // this is unlike the default conversion for objects and arrays + // passed to an expression. + const expressionParams = params.map((value: any) => { + if (value instanceof Expr) { + return value; + } + if (value instanceof AggregateFunction) { + return value; + } else if (isPlainObject(value)) { + return _mapValue(value); + } else { + return constant(value); + } + }); + + expressionParams.forEach(param => { if (isReadableUserData(param)) { param._readUserData(this.userDataReader); } }); - copy.push(new GenericStage(name, params)); + return this._addStage(new GenericStage(name, expressionParams)); + } + + /** + * @internal + * @private + */ + _toProto(jsonProtoSerializer: JsonProtoSerializer): ProtoPipeline { + const stages: ProtoStage[] = this.stages.map(stage => + stage._toProto(jsonProtoSerializer) + ); + return { stages }; + } + + private _addStage(stage: Stage): Pipeline { + const copy = this.stages.map(s => s); + copy.push(stage); return this.newPipeline( this._db, this.userDataReader, @@ -690,74 +805,63 @@ export class Pipeline implements ProtoSerializable { ); } + private selectablesToMap( + selectables: Array + ): Map { + const result = new Map(); + for (const selectable of selectables) { + if (typeof selectable === 'string') { + result.set(selectable as string, field(selectable)); + } else if (selectable instanceof Field) { + result.set((selectable as Field).fieldName(), selectable); + } else if (selectable instanceof ExprWithAlias) { + const expr = selectable as ExprWithAlias; + result.set(expr.alias, expr.expr); + } + } + return result; + } + /** - * 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 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 futureResults = await firestore.pipeline().collection("books") - * .where(gt(Field.of("rating"), 4.5)) - * .select("title", "author", "rating") - * .execute(); - * ``` - * - * @return A Promise representing the asynchronous pipeline execution. + * Reads user data for each expression in the expressionMap. + * @param name Name of the calling function. Used for error messages when invalid user data is encountered. + * @param expressionMap + * @return the expressionMap argument. + * @private */ - execute(): Promise { - const datastore = getDatastore(this._db); - return invokeExecutePipeline(datastore, this).then(result => { - 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( - this._userDataWriter, - element.key?.path - ? new DocumentReference(this._db, null, element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - ) - ); - - return docs; - }); + private readUserData< + T extends + | Map + | ReadableUserData[] + | ReadableUserData + >(name: string, expressionMap: T): T { + if (isReadableUserData(expressionMap)) { + expressionMap._readUserData(this.userDataReader); + } else if (Array.isArray(expressionMap)) { + expressionMap.forEach(readableData => + readableData._readUserData(this.userDataReader) + ); + } else { + expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); + } + return expressionMap; } /** * @internal * @private + * @param db + * @param userDataReader + * @param userDataWriter + * @param stages + * @protected */ - _toProto(jsonProtoSerializer: JsonProtoSerializer): ExecutePipelineRequest { - const stages: ProtoStage[] = this.stages.map(stage => - stage._toProto(jsonProtoSerializer) - ); - const structuredPipeline: StructuredPipeline = { pipeline: { stages } }; - return { - database: getEncodedDatabaseId(jsonProtoSerializer), - structuredPipeline - }; + protected newPipeline( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + stages: Stage[] + ): Pipeline { + return new Pipeline(db, userDataReader, userDataWriter, stages); } } diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index 98b121ad485..6179a35b8b6 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -15,11 +15,14 @@ * limitations under the License. */ +import { invokeExecutePipeline } from '../remote/datastore'; + +import { getDatastore } from './components'; import { Firestore } from './database'; import { Pipeline } from './pipeline'; -import { PipelineResult } from './pipeline-result'; +import { PipelineResult, PipelineSnapshot } from './pipeline-result'; import { PipelineSource } from './pipeline-source'; -import { Query } from './reference'; +import { DocumentReference } from './reference'; import { LiteUserDataWriter } from './reference_impl'; import { Stage } from './stage'; import { newUserDataReader } from './user_data_reader'; @@ -30,67 +33,71 @@ declare module './database' { } } -declare module './reference' { - interface Query { - pipeline(): Pipeline; - } -} - -/** - * Modular API for console experimentation. - * @param pipeline Execute this pipeline. - * @beta - */ -export function execute(pipeline: Pipeline): Promise { - return pipeline.execute(); -} - -/** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - /** - * Experimental Modular API for console testing. - * @param query + * 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 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 futureResults = await execute(firestore.pipeline().collection("books") + * .where(gt(field("rating"), 4.5)) + * .select("title", "author", "rating")); + * ``` + * + * @param pipeline The pipeline to execute. + * @return A Promise representing the asynchronous pipeline execution. */ -export function pipeline(query: Query): Pipeline; - -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - if (firestoreOrQuery instanceof Firestore) { - const db = firestoreOrQuery; - const userDataWriter = new LiteUserDataWriter(db); - const userDataReader = newUserDataReader(db); - return new PipelineSource((stages: Stage[]) => { - return new Pipeline(db, userDataReader, userDataWriter, stages); - }); - } else { - let pipeline; - const query = firestoreOrQuery; - if (query._query.collectionGroup) { - pipeline = query.firestore - .pipeline() - .collectionGroup(query._query.collectionGroup); - } else { - pipeline = query.firestore - .pipeline() - .collection(query._query.path.canonicalString()); - } +export function execute(pipeline: Pipeline): Promise { + const datastore = getDatastore(pipeline._db); + return invokeExecutePipeline(datastore, pipeline).then(result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; - // TODO(pipeline) convert existing query filters, limits, etc into - // pipeline stages + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.key?.path + ? new DocumentReference(pipeline._db, null, element.key) + : undefined, + element.fields, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) + ); - return pipeline; - } + return new PipelineSnapshot(pipeline, docs, executionTime); + }); } Firestore.prototype.pipeline = function (): PipelineSource { - return pipeline(this); -}; - -Query.prototype.pipeline = function (): Pipeline { - return pipeline(this); + const userDataWriter = new LiteUserDataWriter(this); + const userDataReader = newUserDataReader(this); + return new PipelineSource(this._databaseId, (stages: Stage[]) => { + return new Pipeline(this, userDataReader, userDataWriter, stages); + }); }; diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 3024e2e9db0..66c3a1422e9 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -23,6 +23,7 @@ import { FieldPath as InternalFieldPath } from '../model/path'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; +import { Field } from './expressions'; import { FieldPath } from './field_path'; import { DocumentData, @@ -515,12 +516,14 @@ export function snapshotEqual( */ export function fieldPathFromArgument( methodName: string, - arg: string | FieldPath | Compat + arg: string | FieldPath | Compat | Field ): InternalFieldPath { if (typeof arg === 'string') { return fieldPathFromDotSeparatedString(methodName, arg); } else if (arg instanceof FieldPath) { return arg._internalPath; + } else if (arg instanceof Field) { + return fieldPathFromDotSeparatedString(methodName, arg.fieldName()); } else { return arg._delegate._internalPath; } diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts index 46f8fe60654..c5e1cbdbfa5 100644 --- a/packages/firestore/src/lite-api/stage.ts +++ b/packages/firestore/src/lite-api/stage.ts @@ -25,17 +25,20 @@ import { JsonProtoSerializer, ProtoSerializable, toMapValue, + toPipelineValue, toStringValue } from '../remote/serializer'; import { hardAssert } from '../util/assert'; import { - Accumulator, + AggregateFunction, Expr, Field, - FilterCondition, - Ordering + BooleanExpr, + Ordering, + field } from './expressions'; +import { Pipeline } from './pipeline'; import { DocumentReference } from './reference'; import { VectorValue } from './vector_value'; @@ -66,6 +69,26 @@ export class AddFields implements Stage { } } +/** + * @beta + */ +export class RemoveFields implements Stage { + name = 'remove_fields'; + + constructor(private fields: Field[]) {} + + /** + * @internal + * @private + */ + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: this.fields.map(f => f._toProto(serializer)) + }; + } +} + /** * @beta */ @@ -73,7 +96,7 @@ export class Aggregate implements Stage { name = 'aggregate'; constructor( - private accumulators: Map, + private accumulators: Map, private groups: Map ) {} @@ -181,8 +204,16 @@ export class DocumentsSource implements Stage { constructor(private docPaths: string[]) {} - static of(refs: DocumentReference[]): DocumentsSource { - return new DocumentsSource(refs.map(ref => '/' + ref.path)); + static of(refs: Array): DocumentsSource { + return new DocumentsSource( + refs.map(ref => + ref instanceof DocumentReference + ? '/' + ref.path + : ref.startsWith('/') + ? ref + : '/' + ref + ) + ); } /** @@ -205,7 +236,7 @@ export class DocumentsSource implements Stage { export class Where implements Stage { name = 'where'; - constructor(private condition: FilterCondition) {} + constructor(private condition: BooleanExpr) {} /** * @internal @@ -267,9 +298,7 @@ export class FindNearest implements Stage { if (this._distanceField) { // eslint-disable-next-line camelcase - options.distance_field = Field.of(this._distanceField)._toProto( - serializer - ); + options.distance_field = field(this._distanceField)._toProto(serializer); } return { @@ -372,18 +401,108 @@ export class Sort implements Stage { } } +/** + * @beta + */ +export class Sample implements Stage { + name = 'sample'; + + constructor(private limit: number, private mode: string) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toNumber(serializer, this.limit)!, toStringValue(this.mode)!] + }; + } +} + +/** + * @beta + */ +export class Union implements Stage { + name = 'union'; + + constructor(private _other: Pipeline) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [toPipelineValue(this._other._toProto(serializer))] + }; + } +} + +/** + * @beta + */ +export class Unnest implements Stage { + name = 'unnest'; + constructor( + private expr: Expr, + private alias: Field, + private indexField?: string + ) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + const stageProto: ProtoStage = { + name: this.name, + args: [this.expr._toProto(serializer), this.alias._toProto(serializer)] + }; + + if (this.indexField) { + stageProto.options = { + indexField: toStringValue(this.indexField) + }; + } + + return stageProto; + } +} + +/** + * @beta + */ +export class Replace implements Stage { + name = 'replace'; + + constructor( + private field: Field, + private mode: + | 'full_replace' + | 'merge_prefer_nest' + | 'merge_prefer_parent' = 'full_replace' + ) {} + + _toProto(serializer: JsonProtoSerializer): ProtoStage { + return { + name: this.name, + args: [this.field._toProto(serializer), toStringValue(this.mode)] + }; + } +} + /** * @beta */ export class GenericStage implements Stage { - constructor(public name: string, params: unknown[]) {} + /** + * @private + * @internal + */ + constructor( + public name: string, + private params: Array + ) {} /** * @internal * @private */ _toProto(serializer: JsonProtoSerializer): ProtoStage { - // TODO support generic stage - return {}; + return { + name: this.name, + args: this.params.map(o => o._toProto(serializer)) + }; } } diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 42f905d5cab..733fa9050cd 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -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'; @@ -909,6 +910,8 @@ export 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)}` @@ -955,7 +958,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 && @@ -966,7 +969,8 @@ function looksLikeJsonObject(input: unknown): boolean { !(input instanceof Bytes) && !(input instanceof DocumentReference) && !(input instanceof FieldValue) && - !(input instanceof VectorValue) + !(input instanceof VectorValue) && + !isProtoValueSerializable(input) ); } diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 00c1e7fca9e..32666feeea1 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -59,7 +59,8 @@ import { toQueryTarget, toResourcePath, toRunAggregationQueryRequest, - fromPipelineResponse + fromPipelineResponse, + getEncodedDatabaseId } from './serializer'; /** @@ -244,7 +245,12 @@ export async function invokeExecutePipeline( pipeline: Pipeline ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); - const executePipelineRequest = pipeline._toProto(datastoreImpl.serializer); + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: getEncodedDatabaseId(datastoreImpl.serializer), + structuredPipeline: { + pipeline: pipeline._toProto(datastoreImpl.serializer) + } + }; const response = await datastoreImpl.invokeStreamingRPC< ProtoExecutePipelineRequest, diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 4759571b4a5..095e377eba6 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -94,7 +94,8 @@ import { WriteResult as ProtoWriteResult, Value as ProtoValue, MapValue as ProtoMapValue, - ExecutePipelineResponse as ProtoExecutePipelineResponse + ExecutePipelineResponse as ProtoExecutePipelineResponse, + Pipeline } from '../protos/firestore_proto_api'; import { debugAssert, fail, hardAssert } from '../util/assert'; import { ByteString } from '../util/byte_string'; @@ -1433,6 +1434,21 @@ 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 ( + typeof value._toProto === 'function' && + value._protoValueType === 'ProtoValue' + ); +} + export interface UserData { _readUserData(dataReader: UserDataReader): void; } @@ -1466,6 +1482,10 @@ 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 diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index c298bfe2131..361ebda1935 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -51,6 +51,10 @@ export function isSafeInteger(value: unknown): boolean { ); } +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + /** The subset of the browser's Window interface used by the SDK. */ export interface WindowLike { readonly localStorage: Storage; diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 48441c26065..2ed82824c2f 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -18,13 +18,26 @@ import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { Bytes, vector } from '../../../src/api'; -import { GeoPoint } from '../../../src/lite-api/geo_point'; -import { Timestamp } from '../../../src/lite-api/timestamp'; +import { + AggregateFunction, + BooleanExpr, + constantVector, + FunctionExpr +} from '../../../src/lite-api/expressions'; +import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { Deferred } from '../../util/promise'; import { - pipeline, + GeoPoint, + Timestamp, + array, + descending, + isNan, + map, + Bytes, + getFirestore, + terminate, + vector, execute, _internalPipelineToExecutePipelineRequestProto, add, @@ -33,7 +46,6 @@ import { arrayContainsAny, avgFunction, CollectionReference, - Constant, cosineDistance, countAll, doc, @@ -42,7 +54,6 @@ import { endsWith, eq, euclideanDistance, - Field, Firestore, gt, like, @@ -50,9 +61,7 @@ import { lte, mapGet, neq, - not, orFunction, - PipelineResult, regexContains, regexMatch, setDoc, @@ -62,30 +71,38 @@ import { cond, eqAny, logicalMaximum, - logicalMinimum, notEqAny, - query, - where, - FieldPath, - orderBy, - limit, - limitToLast, - startAt, - startAfter, - endAt, - endBefore, - collectionGroup, collection, - and, + multiply, + countIf, + bitAnd, + bitOr, + bitXor, + bitNot, + bitLeftShift, + bitRightShift, + rand, + arrayOffset, + currentContext, + isError, + ifError, + isAbsent, + isNull, + isNotNull, + isNotNan, + mapRemove, + mapMerge, + documentIdFunction, + substr, + manhattanDistance, documentId, - addDoc, - getDoc + logicalMinimum, + xor, + field, + constant, + writeBatch } from '../util/firebase_export'; -import { - apiDescribe, - PERSISTENCE_MODE_UNSPECIFIED, - withTestCollection -} from '../util/helpers'; +import { apiDescribe, withTestCollection } from '../util/helpers'; use(chaiAsPromised); @@ -94,236 +111,667 @@ setLogLevel('debug'); apiDescribe.only('Pipelines', persistence => { addEqualityMatcher(); - describe('books tests', () => { - let firestore: Firestore; - let randomCol: CollectionReference; - - async function testCollectionWithDocs(docs: { - [id: string]: DocumentData; - }): Promise> { - for (const id in docs) { - if (docs.hasOwnProperty(id)) { - const ref = doc(randomCol, id); - await setDoc(ref, docs[id]); - } + 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.ref?.id); + expect(actualIds).to.deep.equal(data); + } else { + docs.forEach(r => { + expect(r.data()).to.deep.equal(data.shift()); + }); } - return randomCol; } + } + + 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 } } + }, + book2: { + title: 'Pride and Prejudice', + author: 'Jane Austen', + genre: 'Romance', + published: 1813, + rating: 4.5, + tags: ['classic', 'social commentary', 'love'], + awards: { none: true } + }, + 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 } + }, + 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 + }, + 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 } + }, + book6: { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + genre: 'Psychological Thriller', + published: 1866, + rating: 4.3, + tags: ['philosophy', 'crime', 'redemption'], + awards: { none: true } + }, + 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 } + }, + book8: { + title: '1984', + author: 'George Orwell', + genre: 'Dystopian', + published: 1949, + rating: 4.2, + tags: ['surveillance', 'totalitarianism', 'propaganda'], + awards: { prometheus: true } + }, + book9: { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + genre: 'Modernist', + published: 1925, + rating: 4.0, + tags: ['wealth', 'american dream', 'love'], + awards: { none: true } + }, + book10: { + title: 'Dune', + author: 'Frank Herbert', + genre: 'Science Fiction', + published: 1965, + rating: 4.6, + tags: ['politics', 'desert', 'ecology'], + awards: { hugo: true, nebula: true } + } + }; + 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; + } + ); - function expectResults( - result: Array>, - ...docs: string[] - ): void; - function expectResults( - result: Array>, - ...data: DocumentData[] - ): void; - - function expectResults( - result: Array>, - ...data: DocumentData[] | string[] - ): void { - expect(result.length).to.equal(data.length); - - if (data.length > 0) { - if (typeof data[0] === 'string') { - const actualIds = result.map(result => result.ref?.id); - expect(actualIds).to.deep.equal(data); - } else { - result.forEach(r => { - expect(r.data()).to.deep.equal(data.shift()); - }); + await setupDeferred.promise; + }); + + afterEach(async () => { + testDeferred?.resolve(); + await withTestCollectionPromise; + }); + + 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 snapshot = await execute( + firestore.pipeline().collection(randomCol.path) + ); + expect(snapshot.results.length).to.equal(10); + }); + + 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('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( + andFunction( + eq('metadataArray', [ + 1, + 2, + field('genre'), + multiply('rating', 10), + [field('title')], + { + published: field('published') + } + ]), + eq('metadata', { + genre: field('genre'), + rating: multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: { + published: field('published') + } + }) + ) + ) + ); + + expect(snapshot.results.length).to.equal(1); + + expectResults(snapshot, { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + genre: 'Fantasy', + published: 1954, + rating: 4.7, + tags: ['adventure', 'magic', 'epic'], + awards: { hugo: false, nebula: false }, + metadataArray: [ + 1, + 2, + 'Fantasy', + 47, + ['The Lord of the Rings'], + { + published: 1954 + } + ], + metadata: { + genre: 'Fantasy', + rating: 47, + nestedArray: ['The Lord of the Rings'], + nestedMap: { + published: 1954 } } - } + }); + }); - 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 } } - }, - book2: { - title: 'Pride and Prejudice', - author: 'Jane Austen', - genre: 'Romance', - published: 1813, - rating: 4.5, - tags: ['classic', 'social commentary', 'love'], - awards: { none: true } + 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' }, - 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 } - }, - 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 } - }, - 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 } - }, - book6: { - title: 'Crime and Punishment', - author: 'Fyodor Dostoevsky', - genre: 'Psychological Thriller', - published: 1866, - rating: 4.3, - tags: ['philosophy', 'crime', 'redemption'], - awards: { none: true } - }, - 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 } - }, - book8: { - title: '1984', - author: 'George Orwell', - genre: 'Dystopian', - published: 1949, - rating: 4.2, - tags: ['surveillance', 'totalitarianism', 'propaganda'], - awards: { prometheus: true } - }, - book9: { - title: 'The Great Gatsby', - author: 'F. Scott Fitzgerald', - genre: 'Modernist', - published: 1925, - rating: 4.0, - tags: ['wealth', 'american dream', 'love'], - awards: { none: true } + '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' }, - book10: { - title: 'Dune', - author: 'Frank Herbert', - genre: 'Science Fiction', - published: 1965, - rating: 4.6, - tags: ['politics', 'desert', 'ecology'], - awards: { hugo: true, nebula: true } + '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' } - }; - 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; - } + it('supports internal serialization to proto', async () => { + const pipeline = firestore + .pipeline() + .collection('books') + .where(eq('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') ); - await setupDeferred.promise; + const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); + expect(proto).not.to.be.null; + }); + + describe('timestamps', () => { + 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, + end - start + ); }); - afterEach(async () => { - testDeferred?.resolve(); - await withTestCollectionPromise; + 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, + end - start + ); }); - describe('fluent API', () => { - it('empty results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .limit(0) - .execute(); - expect(result.length).to.equal(0); + 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, + endDocCreation - beginDocCreation + ); + expect(doc.updateTime!.toDate().valueOf()).to.approximately( + (beginDocCreation + endDocCreation) / 2, + endDocCreation - beginDocCreation + ); + expect(doc.createTime?.valueOf()).to.equal(doc.updateTime?.valueOf()); }); - it('full results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .execute(); - expect(result.length).to.equal(10); + 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 aggregate results as expected', async () => { - let result = await firestore - .pipeline() - .collection(randomCol.path) - .aggregate(countAll().as('count')) - .execute(); - expectResults(result, { count: 10 }); + it('returns execution time for an aggregate query', async () => { + const start = new Date().valueOf(); + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate(avgFunction('rating').as('avgRating')); - result = await randomCol - .pipeline() - .where(eq('genre', 'Science Fiction')) - .aggregate( - countAll().as('count'), - avgFunction('rating').as('avgRating'), - Field.of('rating').maximum().as('maxRating') - ) - .execute(); - expectResults(result, { count: 2, avgRating: 4.4, maxRating: 4.6 }); + 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, + end - start + ); + }); + + it('returns undefined create and update time for each result in an aggregate query', async () => { + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [avgFunction('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('stages', () => { + describe('aggregate stage', () => { + it('supports aggregate', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countAll().as('count')) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + avgFunction('rating').as('avgRating'), + field('rating').maximum().as('maxRating') + ) + ); + expectResults(snapshot, { count: 2, avgRating: 4.4, maxRating: 4.6 }); }); it('rejects groups without accumulators', async () => { await expect( - randomCol + execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(lt('published', 1900)) + .aggregate({ + accumulators: [], + groups: ['genre'] + }) + ) + ).to.be.rejected; + }); + + it('returns group and accumulate results', async () => { + const snapshot = await execute( + firestore .pipeline() - .where(lt('published', 1900)) + .collection(randomCol.path) + .where(lt(field('published'), 1984)) .aggregate({ - accumulators: [], + accumulators: [avgFunction('rating').as('avgRating')], groups: ['genre'] }) - .execute() - ).to.be.rejected; + .where(gt('avgRating', 4.3)) + .sort(field('avgRating').descending()) + ); + expectResults( + snapshot, + { avgRating: 4.7, genre: 'Fantasy' }, + { avgRating: 4.5, genre: 'Romance' }, + { avgRating: 4.4, genre: 'Science Fiction' } + ); + }); + + it('returns min and max accumulations', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + countAll().as('count'), + field('rating').maximum().as('maxRating'), + field('published').minimum().as('minPublished') + ) + ); + expectResults(snapshot, { + count: 10, + maxRating: 4.7, + minPublished: 1813 + }); + }); + + it('returns countif accumulation', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countIf(field('rating').gt(4.3)).as('count')) + ); + const expectedResults = { + count: 3 + }; + expectResults(snapshot, expectedResults); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(field('rating').gt(4.3).countIf().as('count')) + ); + expectResults(snapshot, expectedResults); }); + }); + describe('distinct stage', () => { it('returns distinct values as expected', async () => { - const results = await randomCol - .pipeline() - .distinct('genre', 'author') - .sort(Field.of('genre').ascending(), Field.of('author').ascending()) - .execute(); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort(field('genre').ascending(), field('author').ascending()) + ); expectResults( - results, + snapshot, { genre: 'Dystopian', author: 'George Orwell' }, { genre: 'Dystopian', author: 'Margaret Atwood' }, { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, @@ -336,51 +784,19 @@ apiDescribe.only('Pipelines', persistence => { { genre: 'Southern Gothic', author: 'Harper Lee' } ); }); + }); - it('returns group and accumulate results', async () => { - const results = await randomCol - .pipeline() - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()) - .execute(); - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } - ); - }); - - it('returns min and max accumulations', async () => { - const results = await randomCol - .pipeline() - .aggregate( - countAll().as('count'), - Field.of('rating').maximum().as('maxRating'), - Field.of('published').minimum().as('minPublished') - ) - .execute(); - expectResults(results, { - count: 10, - maxRating: 4.7, - minPublished: 1813 - }); - }); - + describe('select stage', () => { it('can select fields', async () => { - const results = await firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'author') - .sort(Field.of('author').ascending()) - .execute(); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + ); expectResults( - results, + snapshot, { title: "The Hitchhiker's Guide to the Galaxy", author: 'Douglas Adams' @@ -399,234 +815,614 @@ apiDescribe.only('Pipelines', persistence => { { title: "The Handmaid's Tale", author: 'Margaret Atwood' } ); }); + }); - it('where with and', async () => { - const results = await randomCol - .pipeline() - .where(andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction'))) - .execute(); - expectResults(results, 'book10'); + 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('where with or', async () => { - const results = await randomCol - .pipeline() - .where(orFunction(eq('genre', 'Romance'), eq('genre', 'Dystopian'))) - .select('title') - .execute(); + 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( - results, + 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" }, - { title: '1984' } + { + title: "The Handmaid's Tale" + } ); }); + }); - it('offset and limits', async () => { - const results = await firestore - .pipeline() - .collection(randomCol.path) - .sort(Field.of('author').ascending()) - .offset(5) - .limit(3) - .select('title', 'author') - .execute(); - expectResults( - results, - { 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('where stage', () => { + it('where with and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + andFunction( + gt('rating', 4.5), + eq('genre', 'Science Fiction'), + lte('published', 1965) + ) + ) ); + expectResults(snapshot, 'book10'); }); - - it('logical min works', async () => { - const results = await randomCol - .pipeline() - .select( - 'title', - logicalMinimum(Constant.of(1960), Field.of('published')).as( - 'published-safe' + it('where with or', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + orFunction( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy') + ) ) - ) - .sort(Field.of('title').ascending()) - .limit(3) - .execute(); + .select('title') + ); expectResults( - results, - { title: '1984', 'published-safe': 1949 }, - { title: 'Crime and Punishment', 'published-safe': 1866 }, - { title: 'Dune', 'published-safe': 1960 } + snapshot, + { title: 'Pride and Prejudice' }, + { title: 'The Lord of the Rings' }, + { title: "The Handmaid's Tale" }, + { title: '1984' } ); }); - it('logical max works', async () => { - const results = await randomCol - .pipeline() - .select( - 'title', - logicalMaximum(Constant.of(1960), Field.of('published')).as( - 'published-safe' + it('where with xor', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + xor( + eq('genre', 'Romance'), + eq('genre', 'Dystopian'), + eq('genre', 'Fantasy'), + eq('published', 1949) + ) ) - ) - .sort(Field.of('title').ascending()) - .limit(3) - .execute(); + .select('title') + ); expectResults( - results, - { title: '1984', 'published-safe': 1960 }, - { title: 'Crime and Punishment', 'published-safe': 1960 }, - { title: 'Dune', 'published-safe': 1965 } + snapshot, + { title: 'Pride and Prejudice' }, + { title: 'The Lord of the Rings' }, + { title: "The Handmaid's Tale" } ); }); + }); - it('accepts and returns all data types', async () => { - const refDate = new Date(); - const refTimestamp = Timestamp.now(); - const constants = [ - Constant.of(1).as('number'), - Constant.of('a string').as('string'), - Constant.of(true).as('boolean'), - Constant.of(null).as('null'), - Constant.of(new GeoPoint(0.1, 0.2)).as('geoPoint'), - Constant.of(refTimestamp).as('timestamp'), - Constant.of(refDate).as('date'), - Constant.of( - Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) - ).as('bytes'), - Constant.of(doc(firestore, 'foo', 'bar')).as('documentReference'), - Constant.of(vector([1, 2, 3])).as('vectorValue'), - Constant.of({ - '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'), - Constant.of([ - 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') - ]; + 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' } + ); + }); + }); - const results = await randomCol - .pipeline() - .limit(1) - .select(...constants) - .execute(); - - expectResults(results, { - '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'] + describe('generic stage', () => { + it('can select fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .genericStage('select', [ + { + title: field('title'), + metadata: { + 'author': field('author') + } + } + ]) + .sort(field('author').ascending()) + .limit(1) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + metadata: { + author: 'Douglas Adams' + } + }); + }); + + it('can add fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('author').ascending()) + .limit(1) + .select('title', 'author') + .genericStage('add_fields', [ + { + display: field('title').strConcat(' - ', field('author')) + } + ]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + display: "The Hitchhiker's Guide to the Galaxy - Douglas Adams" + }); + }); + + it('can filter with where', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('where', [field('author').eq('Douglas Adams')]) + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }); + }); + + it('can limit, offset, and sort', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .genericStage('sort', [ + { + direction: 'ascending', + expression: field('author') + } + ]) + .genericStage('offset', [3]) + .genericStage('limit', [1]) + ); + expectResults(snapshot, { + author: 'Fyodor Dostoevsky', + title: 'Crime and Punishment' + }); + }); + + it('can perform aggregate query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('aggregate', [ + { averageRating: field('rating').avg() }, + {} + ]) + ); + expectResults(snapshot, { + averageRating: 4.3100000000000005 + }); + }); + + it('can perform distinct query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'rating') + .genericStage('distinct', [{ rating: field('rating') }]) + .sort(field('rating').descending()) + ); + expectResults( + snapshot, + { + rating: 4.7 }, - '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' - } - ] + { + rating: 4.6 + }, + { + rating: 4.5 + }, + { + rating: 4.3 + }, + { + rating: 4.2 + }, + { + rating: 4.1 + }, + { + rating: 4.0 + } + ); + }); + }); + + describe('replace stage', () => { + it('run pipleine with replace', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith('awards') + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } }); }); + }); + + describe('sample stage', () => { + it('run pipeline with sample limit of 3', async () => { + const snapshot = await execute( + firestore.pipeline().collection(randomCol.path).sample(3) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {documents: 3}', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ documents: 3 }) + ); + expect(snapshot.results.length).to.equal(3); + }); + + it('run pipeline with sample limit of {percentage: 0.6}', async () => { + let avgSize = 0; + const numIterations = 20; + for (let i = 0; i < numIterations; i++) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sample({ percentage: 0.6 }) + ); + + avgSize += snapshot.results.length; + } + avgSize /= numIterations; + expect(avgSize).to.be.closeTo(6, 1); + }); + }); + + describe('union stage', () => { + it('run pipeline with union', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union(firestore.pipeline().collection(randomCol.path)) + .sort(field(documentId()).ascending()) + ); + expectResults( + snapshot, + 'book1', + 'book1', + 'book10', + 'book10', + 'book2', + 'book2', + 'book3', + 'book3', + 'book4', + 'book4', + 'book5', + 'book5', + 'book6', + 'book6', + 'book7', + 'book7', + 'book8', + 'book8', + 'book9', + 'book9' + ); + }); + }); - it('cond works', async () => { - const results = await randomCol + describe('unnest stage', () => { + it('run pipeline with unnest', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag')) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + it('unnest an expr', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(array([1, 2, 3]).as('copy')) + ); + 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('function expressions', () => { + it('logical max works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMaximum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1961 }, + { title: 'Crime and Punishment', 'published-safe': 1961 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); + + it('logical min works', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) + .select( + 'title', + logicalMinimum(constant(1960), field('published'), 1961).as( + 'published-safe' + ) + ) + .sort(field('title').ascending()) + .limit(3) + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1949 }, + { title: 'Crime and Punishment', 'published-safe': 1866 }, + { title: 'Dune', 'published-safe': 1960 } + ); + }); + + it('cond works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) .select( 'title', cond( - lt(Field.of('published'), 1960), - Constant.of(1960), - Field.of('published') + lt(field('published'), 1960), + constant(1960), + field('published') ).as('published-safe') ) - .sort(Field.of('title').ascending()) + .sort(field('title').ascending()) .limit(3) - .execute(); - expectResults( - results, - { title: '1984', 'published-safe': 1960 }, - { title: 'Crime and Punishment', 'published-safe': 1960 }, - { title: 'Dune', 'published-safe': 1965 } - ); - }); + ); + expectResults( + snapshot, + { title: '1984', 'published-safe': 1960 }, + { title: 'Crime and Punishment', 'published-safe': 1960 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); - it('eqAny works', async () => { - const results = await randomCol + it('eqAny works', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(eqAny('published', [1979, 1999, 1967])) .select('title') - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'One Hundred Years of Solitude' } - ); - }); + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'One Hundred Years of Solitude' } + ); + }); - it('notEqAny works', async () => { - const results = await randomCol + it('notEqAny works', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where( notEqAny( 'published', @@ -634,256 +1430,230 @@ apiDescribe.only('Pipelines', persistence => { ) ) .select('title') - .execute(); - expectResults(results, { title: 'Pride and Prejudice' }); - }); + ); + expectResults(snapshot, { title: 'Pride and Prejudice' }); + }); - it('arrayContains works', async () => { - const results = await randomCol + it('arrayContains works', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(arrayContains('tags', 'comedy')) .select('title') - .execute(); - expectResults(results, { - title: "The Hitchhiker's Guide to the Galaxy" - }); + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" }); + }); - it('arrayContainsAny works', async () => { - const results = await randomCol + it('arrayContainsAny works', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(arrayContainsAny('tags', ['comedy', 'classic'])) .select('title') - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'Pride and Prejudice' } - ); - }); + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'Pride and Prejudice' } + ); + }); - it('arrayContainsAll works', async () => { - const results = await randomCol + it('arrayContainsAll works', async () => { + const snapshot = await execute( + firestore .pipeline() - .where(Field.of('tags').arrayContainsAll('adventure', 'magic')) + .collection(randomCol.path) + .where(field('tags').arrayContainsAll(['adventure', 'magic'])) .select('title') - .execute(); - expectResults(results, { title: 'The Lord of the Rings' }); - }); + ); + expectResults(snapshot, { title: 'The Lord of the Rings' }); + }); - it('arrayLength works', async () => { - const results = await randomCol + it('arrayLength works', async () => { + const snapshot = await execute( + firestore .pipeline() - .select(Field.of('tags').arrayLength().as('tagsCount')) + .collection(randomCol.path) + .select(field('tags').arrayLength().as('tagsCount')) .where(eq('tagsCount', 3)) - .execute(); - expect(results.length).to.equal(10); - }); + ); + expect(snapshot.results.length).to.equal(10); + }); - // skip: arrayConcat not supported - // it.skip('arrayConcat works', async () => { - // const results = await randomCol - // .pipeline() - // .select( - // Field.of('tags').arrayConcat(['newTag1', 'newTag2']).as('modifiedTags') - // ) - // .limit(1) - // .execute(); - // expectResults(results, { - // modifiedTags: ['comedy', 'space', 'adventure', 'newTag1', 'newTag2'] - // }); - // }); - - it('testStrConcat', async () => { - const results = await randomCol + it('testStrConcat', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .select( - Field.of('author') - .strConcat(' - ', Field.of('title')) - .as('bookInfo') + field('author').strConcat(' - ', field('title')).as('bookInfo') ) .limit(1) - .execute(); - expectResults(results, { - bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" - }); + ); + expectResults(snapshot, { + bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" }); + }); - it('testStartsWith', async () => { - const results = await randomCol + it('testStartsWith', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(startsWith('title', 'The')) .select('title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { title: 'The Great Gatsby' }, - { title: "The Handmaid's Tale" }, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'The Lord of the Rings' } - ); - }); + .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 results = await randomCol + it('testEndsWith', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(endsWith('title', 'y')) .select('title') - .sort(Field.of('title').descending()) - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'The Great Gatsby' } - ); - }); + .sort(field('title').descending()) + ); + expectResults( + snapshot, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'The Great Gatsby' } + ); + }); - it('testLength', async () => { - const results = await randomCol + it('testLength', async () => { + const snapshot = await execute( + firestore .pipeline() - .select( - Field.of('title').charLength().as('titleLength'), - Field.of('title') - ) + .collection(randomCol.path) + .select(field('title').charLength().as('titleLength'), field('title')) .where(gt('titleLength', 20)) - .sort(Field.of('title').ascending()) - .execute(); + .sort(field('title').ascending()) + ); - expectResults( - results, + 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' - } - ); - }); + { + 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' + } + ); + }); - // skip: toLower not supported - // it.skip('testToLowercase', async () => { - // const results = await randomCol - // .pipeline() - // .select(Field.of('title').toLower().as('lowercaseTitle')) - // .limit(1) - // .execute(); - // expectResults(results, { - // lowercaseTitle: "the hitchhiker's guide to the galaxy" - // }); - // }); - - // skip: toUpper not supported - // it.skip('testToUppercase', async () => { - // const results = await randomCol - // .pipeline() - // .select(Field.of('author').toUpper().as('uppercaseAuthor')) - // .limit(1) - // .execute(); - // expectResults(results, { uppercaseAuthor: 'DOUGLAS ADAMS' }); - // }); - - // skip: trim not supported - // it.skip('testTrim', async () => { - // const results = await randomCol - // .pipeline() - // .addFields(strConcat(' ', Field.of('title'), ' ').as('spacedTitle')) - // .select( - // Field.of('spacedTitle').trim().as('trimmedTitle'), - // Field.of('spacedTitle') - // ) - // .limit(1) - // .execute(); - // expectResults(results, { - // spacedTitle: " The Hitchhiker's Guide to the Galaxy ", - // trimmedTitle: "The Hitchhiker's Guide to the Galaxy" - // }); - // }); - - it('testLike', async () => { - const results = await randomCol + it('testLike', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(like('title', '%Guide%')) .select('title') - .execute(); - expectResults(results, { - title: "The Hitchhiker's Guide to the Galaxy" - }); + ); + expectResults(snapshot, { + title: "The Hitchhiker's Guide to the Galaxy" }); + }); - it('testRegexContains', async () => { - const results = await randomCol + it('testRegexContains', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(regexContains('title', '(?i)(the|of)')) - .execute(); - expect(results.length).to.equal(5); - }); + ); + expect(snapshot.results.length).to.equal(5); + }); - it('testRegexMatches', async () => { - const results = await randomCol + it('testRegexMatches', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(regexMatch('title', '.*(?i)(the|of).*')) - .execute(); - expect(results.length).to.equal(5); - }); + ); + expect(snapshot.results.length).to.equal(5); + }); - it('testArithmeticOperations', async () => { - const results = await randomCol + it('testArithmeticOperations', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .select( - add(Field.of('rating'), 1).as('ratingPlusOne'), - subtract(Field.of('published'), 1900).as('yearsSince1900'), - Field.of('rating').multiply(10).as('ratingTimesTen'), - Field.of('rating').divide(2).as('ratingDividedByTwo') + add(field('rating'), 1).as('ratingPlusOne'), + subtract(field('published'), 1900).as('yearsSince1900'), + field('rating').multiply(10).as('ratingTimesTen'), + field('rating').divide(2).as('ratingDividedByTwo'), + multiply('rating', 10, 2).as('ratingTimes20'), + add('rating', 1, 2).as('ratingPlus3') ) .limit(1) - .execute(); - expectResults(results, { - ratingPlusOne: 5.2, - yearsSince1900: 79, - ratingTimesTen: 42, - ratingDividedByTwo: 2.1 - }); + ); + expectResults(snapshot, { + ratingPlusOne: 5.2, + yearsSince1900: 79, + ratingTimesTen: 42, + ratingDividedByTwo: 2.1, + ratingTimes20: 84, + ratingPlus3: 7.2 }); + }); - it('testComparisonOperators', async () => { - const results = await randomCol + it('testComparisonOperators', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where( andFunction( gt('rating', 4.2), - lte(Field.of('rating'), 4.5), + lte(field('rating'), 4.5), neq('genre', 'Science Fiction') ) ) .select('rating', 'title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { rating: 4.3, title: 'Crime and Punishment' }, - { - rating: 4.3, - title: 'One Hundred Years of Solitude' - }, - { rating: 4.5, title: 'Pride and Prejudice' } - ); - }); + .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 results = await randomCol + it('testLogicalOperators', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where( orFunction( andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction')), @@ -891,837 +1661,945 @@ apiDescribe.only('Pipelines', persistence => { ) ) .select('title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { title: 'Crime and Punishment' }, - { title: 'Dune' }, - { title: 'Pride and Prejudice' } - ); - }); + .sort(field('title').ascending()) + ); + expectResults( + snapshot, + { title: 'Crime and Punishment' }, + { title: 'Dune' }, + { title: 'Pride and Prejudice' } + ); + }); - it('testChecks', async () => { - const results = await randomCol + it('testChecks', async () => { + let snapshot = await execute( + firestore .pipeline() - .where(not(Field.of('rating').isNaN())) + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) .select( - Field.of('rating').eq(null).as('ratingIsNull'), - not(Field.of('rating').isNaN()).as('ratingIsNotNaN') + isNull('rating').as('ratingIsNull'), + isNan('rating').as('ratingIsNaN'), + isError(arrayOffset('title', 0)).as('isError'), + ifError(arrayOffset('title', 0), constant('was error')).as( + 'ifError' + ), + isAbsent('foo').as('isAbsent'), + isNotNull('title').as('titleIsNotNull'), + isNotNan('cost').as('costIsNotNan') ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false + }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) .limit(1) - .execute(); - expectResults(results, { ratingIsNull: false, ratingIsNotNaN: true }); + .select( + field('rating').isNull().as('ratingIsNull'), + field('rating').isNan().as('ratingIsNaN'), + arrayOffset('title', 0).isError().as('isError'), + arrayOffset('title', 0) + .ifError(constant('was error')) + .as('ifError'), + field('foo').isAbsent().as('isAbsent'), + field('title').isNotNull().as('titleIsNotNull'), + field('cost').isNotNan().as('costIsNotNan') + ) + ); + expectResults(snapshot, { + ratingIsNull: false, + ratingIsNaN: false, + isError: true, + ifError: 'was error', + isAbsent: true, + titleIsNotNull: true, + costIsNotNan: false }); + }); - it('testMapGet', async () => { - const results = await randomCol + it('testMapGet', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) + .sort(field('published').descending()) .select( - Field.of('awards').mapGet('hugo').as('hugoAward'), - Field.of('awards').mapGet('others').as('others'), - Field.of('title') + field('awards').mapGet('hugo').as('hugoAward'), + field('awards').mapGet('others').as('others'), + field('title') ) .where(eq('hugoAward', true)) - .execute(); - expectResults( - results, - { - hugoAward: true, - title: "The Hitchhiker's Guide to the Galaxy", - others: { unknown: { year: 1980 } } - }, - { hugoAward: true, title: 'Dune', others: null } - ); - }); + ); + expectResults( + snapshot, + { + hugoAward: true, + title: "The Hitchhiker's Guide to the Galaxy", + others: { unknown: { year: 1980 } } + }, + { hugoAward: true, title: 'Dune', others: null } + ); + }); - // it('testParent', async () => { - // const results = await randomCol - // .pipeline() - // .select( - // parent(randomCol.doc('chile').collection('subCollection').path).as( - // 'parent' - // ) - // ) - // .limit(1) - // .execute(); - // expect(results[0].data().parent.endsWith('/books')).to.be.true; - // }); - // - // it('testCollectionId', async () => { - // const results = await randomCol - // .pipeline() - // .select(collectionId(randomCol.doc('chile')).as('collectionId')) - // .limit(1) - // .execute(); - // expectResults(results, {collectionId: 'books'}); - // }); - - it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; - const results = await randomCol + it('testDistanceFunctions', async () => { + const sourceVector = [0.1, 0.1]; + const targetVector = [0.5, 0.8]; + let snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .select( - cosineDistance(Constant.vector(sourceVector), targetVector).as( + cosineDistance(constantVector(sourceVector), targetVector).as( 'cosineDistance' ), - dotProduct(Constant.vector(sourceVector), targetVector).as( + dotProduct(constantVector(sourceVector), targetVector).as( 'dotProductDistance' ), - euclideanDistance(Constant.vector(sourceVector), targetVector).as( + euclideanDistance(constantVector(sourceVector), targetVector).as( 'euclideanDistance' + ), + manhattanDistance(constantVector(sourceVector), targetVector).as( + 'manhattanDistance' ) ) .limit(1) - .execute(); + ); - expectResults(results, { - cosineDistance: 0.02560880430538015, - dotProductDistance: 0.13, - euclideanDistance: 0.806225774829855 - }); + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855, + manhattanDistance: 1.1 }); - it('testNestedFields', async () => { - const results = await randomCol + snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) + .select( + constantVector(sourceVector) + .cosineDistance(targetVector) + .as('cosineDistance'), + constantVector(sourceVector) + .dotProduct(targetVector) + .as('dotProductDistance'), + constantVector(sourceVector) + .euclideanDistance(targetVector) + .as('euclideanDistance'), + constantVector(sourceVector) + .manhattanDistance(targetVector) + .as('manhattanDistance') + ) + .limit(1) + ); + + expectResults(snapshot, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855, + manhattanDistance: 1.1 + }); + }); + + it('testNestedFields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) .where(eq('awards.hugo', true)) .select('title', 'awards.hugo') - .execute(); - expectResults( - results, - { - title: "The Hitchhiker's Guide to the Galaxy", - 'awards.hugo': true - }, - { title: 'Dune', 'awards.hugo': true } - ); - }); + ); + 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 results = await randomCol + it('test mapGet with field name including . notation', async () => { + const snapshot = await execute( + firestore .pipeline() + .collection(randomCol.path) .where(eq('awards.hugo', true)) .select( 'title', - Field.of('nestedField.level.1'), + field('nestedField.level.1'), mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') ) - .execute(); - expectResults( - results, - { - title: "The Hitchhiker's Guide to the Galaxy", - 'nestedField.level.`1`': null, - nested: true - }, - { title: 'Dune', 'nestedField.level.`1`': null, nested: null } + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + 'nestedField.level.`1`': null, + nested: true + }, + { title: 'Dune', 'nestedField.level.`1`': null, nested: null } + ); + }); + + describe('genericFunction', () => { + it('add selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(descending('rating')) + .limit(1) + .select( + new FunctionExpr('add', [field('rating'), constant(1)]).as( + 'rating' + ) + ) ); + expectResults(snapshot, { + rating: 5.7 + }); }); - it('supports internal serialization to proto', async () => { - const pipeline = firestore - .pipeline() - .collection('books') - .where(eq('awards.hugo', true)) - .select( - 'title', - Field.of('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') - ); + it('and (variadic) selectable', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('and', [ + field('rating').gt(0), + field('title').charLength().lt(5), + field('tags').arrayContains('propaganda') + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: '1984' + }); + }); + + it('array contains any', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + new BooleanExpr('array_contains_any', [ + field('tags'), + array(['politics']) + ]) + ) + .select('title') + ); + expectResults(snapshot, { + title: 'Dune' + }); + }); + + it('countif aggregate', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate( + new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( + 'countOfBest' + ) + ) + ); + expectResults(snapshot, { + countOfBest: 3 + }); + }); - const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; + it('sort by char_len', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort( + new FunctionExpr('char_length', [field('title')]).ascending(), + descending('__name__') + ) + .limit(3) + .select('title') + ); + expectResults( + snapshot, + { + title: '1984' + }, + { + title: 'Dune' + }, + { + title: 'The Great Gatsby' + } + ); }); + }); - describe('pagination', () => { - async function addBooks( - collection: CollectionReference - ): Promise { - await setDoc(doc(randomCol, '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 } + describe.skip('not implemented in backend', () => { + it('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitAnd(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 4 + }); + it('supports Bit_and', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitAnd(12).as('result')) + ); + expectResults(snapshot, { + result: 4 }); - await setDoc(doc(randomCol, '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: {} + }); + + it('supports Bit_or', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitOr(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 13 }); - await setDoc(doc(randomCol, '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 } + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitOr(12).as('result')) + ); + expectResults(snapshot, { + result: 13 }); - } + }); - it('supports pagination with filters', async () => { - await addBooks(randomCol); - const pageSize = 2; - const pipeline = randomCol - .pipeline() - .select('title', 'rating', '__name__') - .sort( - Field.of('rating').descending(), - Field.of('__name__').ascending() - ); - - let results = await pipeline.limit(pageSize).execute(); - expectResults( - results, - { title: 'The Lord of the Rings', rating: 4.7 }, - { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 } + it('supports Bit_xor', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(bitXor(constant(5), 12).as('result')) + ); + expectResults(snapshot, { + result: 9 + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(5).bitXor(12).as('result')) ); + expectResults(snapshot, { + result: 9 + }); + }); - const lastDoc = results[results.length - 1]; + it('supports Bit_not', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( + 'result' + ) + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) + .bitNot() + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + }); + }); - results = await pipeline - .where( - orFunction( - andFunction( - Field.of('rating').eq(lastDoc.get('rating')), - Field.of('__path__').gt(lastDoc.ref?.path) - ), - Field.of('rating').lt(lastDoc.get('rating')) + it('supports Bit_left_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitLeftShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitLeftShift(2) + .as('result') ) - ) - .limit(pageSize) - .execute(); - expectResults( - results, - { title: 'Pride and Prejudice', rating: 4.5 }, - { title: 'Crime and Punishment', rating: 4.3 } ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + }); }); - it('supports pagination with offsets', async () => { - await addBooks(randomCol); + it('supports Bit_right_shift', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + bitRightShift( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), + 2 + ).as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) + .bitRightShift(2) + .as('result') + ) + ); + expectResults(snapshot, { + result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + }); + }); - const secondFilterField = '__path__'; + it('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(documentIdFunction(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' + }); + }); - const pipeline = randomCol - .pipeline() - .select('title', 'rating', secondFilterField) - .sort( - Field.of('rating').descending(), - Field.of(secondFilterField).ascending() - ); + it('supports Substr', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); - const pageSize = 2; - let currPage = 0; + it('supports Substr without length', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substr('title', 9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substr(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + }); - let results = await pipeline - .offset(currPage++ * pageSize) - .limit(pageSize) - .execute(); + it('arrayConcat works', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select( + field('tags') + .arrayConcat(['newTag1', 'newTag2'], field('tags'), [null]) + .as('modifiedTags') + ) + .limit(1) + ); + expectResults(snapshot, { + modifiedTags: [ + 'comedy', + 'space', + 'adventure', + 'newTag1', + 'newTag2', + 'comedy', + 'space', + 'adventure', + null + ] + }); + }); - expectResults( - results, - { - title: 'The Lord of the Rings', - rating: 4.7 - }, - { title: 'Dune', rating: 4.6 } + it('testToLowercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(field('title').toLower().as('lowercaseTitle')) + .limit(1) ); + expectResults(snapshot, { + lowercaseTitle: "the hitchhiker's guide to the galaxy" + }); + }); - results = await pipeline - .offset(currPage++ * pageSize) - .limit(pageSize) - .execute(); - expectResults( - results, - { - title: 'Jonathan Strange & Mr Norrell', - rating: 4.6 - }, - { title: 'The Master and Margarita', rating: 4.6 } + it('testToUppercase', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(field('author').toUpper().as('uppercaseAuthor')) + .limit(1) ); + expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + }); - results = await pipeline - .offset(currPage++ * pageSize) - .limit(pageSize) - .execute(); - expectResults( - results, - { - title: 'A Long Way to a Small, Angry Planet', - rating: 4.6 - }, - { - title: 'Pride and Prejudice', - rating: 4.5 - } + it('testTrim', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + constant(" The Hitchhiker's Guide to the Galaxy ").as( + 'spacedTitle' + ) + ) + .select( + field('spacedTitle').trim().as('trimmedTitle'), + field('spacedTitle') + ) + .limit(1) ); + expectResults(snapshot, { + spacedTitle: " The Hitchhiker's Guide to the Galaxy ", + trimmedTitle: "The Hitchhiker's Guide to the Galaxy" + }); }); }); - }); - - describe('modular API', () => { - it('works when creating a pipeline from a Firestore instance', async () => { - const myPipeline = pipeline(firestore) - .collection(randomCol.path) - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()); - const results = await execute(myPipeline); + it('supports Rand', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(10) + .select(rand().as('result')) + ); + expect(snapshot.results.length).to.equal(10); + snapshot.results.forEach(d => { + expect(d.get('result')).to.be.lt(1); + expect(d.get('result')).to.be.gte(0); + }); + }); - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } + 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('works when creating a pipeline from a collection', async () => { - const myPipeline = pipeline(randomCol) - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()); + 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] + }); + }); - const results = await execute(myPipeline); + it('supports arrayOffset', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(arrayOffset('tags', 0).as('firstTag')) + ); + const expectedResults = [ + { + firstTag: 'adventure' + }, + { + firstTag: 'politics' + }, + { + firstTag: 'classic' + } + ]; + expectResults(snapshot, ...expectedResults); - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(3) + .select(field('tags').arrayOffset(0).as('firstTag')) ); + expectResults(snapshot, ...expectedResults); }); }); - }); - - // This is the Query integration tests from the lite API (no cache support) - // with some additional test cases added for more complete coverage. - describe('Query to Pipeline', () => { - function verifyResults( - actual: Array>, - ...expected: DocumentData[] - ): void { - expect(actual.length).to.equal(expected.length); - - for (let i = 0; i < expected.length; ++i) { - expect(actual[i].data()).to.deep.equal(expected[i]); - } - } - it('supports default query', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { 1: { foo: 1 } }, - async collRef => { - const result = await collRef.pipeline().execute(); - verifyResults(result, { foo: 1 }); - } + // TODO: current_context tests with are failing because of b/395937453 + it.skip('supports currentContext', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(currentContext().as('currentContext')) ); + expectResults(snapshot, { + currentContext: 'TODO' + }); }); - it('supports filtered query', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, where('foo', '==', 1)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }); - } + 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') + ) ); - }); - it('supports filtered query (with FieldPath)', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, where(new FieldPath('foo'), '==', 1)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + foo: 'bar' } - ); + }); }); - it('supports ordered query (with default order)', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo')); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }, { foo: 2 }); - } + 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') + ) ); - }); - it('supports ordered query (with asc)', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo', 'asc')); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }, { foo: 2 }); + expect(snapshot.results.length).to.equal(1); + expectResults(snapshot, { + metadata: { + genre: 'Fantasy', + rating: 47 } - ); + }); }); - it('supports ordered query (with desc)', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo', 'desc')); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }, { foo: 1 }); - } + 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')) ); - }); - - it('supports limit query', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), limit(1)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }); - } + 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 limitToLast query', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 }, - 3: { foo: 3 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), limitToLast(2)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }, { foo: 3 }); - } + 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')) ); - }); - - it('supports startAt', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), startAt(2)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }); - } + 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 startAfter (with DocumentReference)', () => { - 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 => { - let docRef = await getDoc(doc(collRef, '2')); - let query1 = query( - collRef, - orderBy('foo'), - orderBy('bar'), - orderBy('baz'), - startAfter(docRef) - ); - let result = await query1.pipeline().execute(); - verifyResults( - result, - { 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 } - ); + 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 } + }); + } - docRef = await getDoc(doc(collRef, '3')); - query1 = query( - collRef, - orderBy('foo'), - orderBy('bar'), - orderBy('baz'), - startAfter(docRef) - ); - result = await query1.pipeline().execute(); - verifyResults( - result, - { 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 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 } ); - }); - it('supports startAt (with DocumentReference)', () => { - 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 => { - let docRef = await getDoc(doc(collRef, '2')); - let query1 = query( - collRef, - orderBy('foo'), - orderBy('bar'), - orderBy('baz'), - startAt(docRef) - ); - let result = await query1.pipeline().execute(); - verifyResults( - result, - { 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 } - ); + const lastDoc = snapshot.results[snapshot.results.length - 1]; - docRef = await getDoc(doc(collRef, '3')); - query1 = query( - collRef, - orderBy('foo'), - orderBy('bar'), - orderBy('baz'), - startAt(docRef) - ); - result = await query1.pipeline().execute(); - verifyResults( - result, - { 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 } - ); - } + snapshot = await execute( + pipeline + .where( + orFunction( + andFunction( + field('rating').eq(lastDoc.get('rating')), + field('__path__').gt(lastDoc.ref?.id) + ), + field('rating').lt(lastDoc.get('rating')) + ) + ) + .limit(pageSize) ); - }); - - it('supports startAfter', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), startAfter(1)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }); - } + expectResults( + snapshot, + { title: 'Pride and Prejudice', rating: 4.5 }, + { title: 'Crime and Punishment', rating: 4.3 } ); }); - it('supports endAt', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), endAt(1)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }); - } - ); - }); + it('supports pagination with offsets', async () => { + await addBooks(randomCol); - it('supports endBefore', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - const query1 = query(collRef, orderBy('foo'), endBefore(2)); - const result = await query1.pipeline().execute(); - verifyResults(result, { foo: 1 }); - } - ); - }); + const secondFilterField = '__path__'; - it('supports pagination', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - { - 1: { foo: 1 }, - 2: { foo: 2 } - }, - async collRef => { - let query1 = query(collRef, orderBy('foo'), limit(1)); - const pipeline1 = query1.pipeline(); - let result = await pipeline1.execute(); - verifyResults(result, { foo: 1 }); - - // Pass the document snapshot from the previous result - query1 = query(query1, startAfter(result[0].get('foo'))); - result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }); - } + 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) ); - }); - it('supports pagination on DocumentIds', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, + expectResults( + snapshot, { - 1: { foo: 1 }, - 2: { foo: 2 } + title: 'The Lord of the Rings', + rating: 4.7 }, - async collRef => { - let query1 = query( - collRef, - orderBy('foo'), - orderBy(documentId(), 'asc'), - limit(1) - ); - const pipeline1 = query1.pipeline(); - let result = await pipeline1.execute(); - verifyResults(result, { foo: 1 }); - - // Pass the document snapshot from the previous result - query1 = query( - query1, - startAfter(result[0].get('foo'), result[0].ref?.id) - ); - result = await query1.pipeline().execute(); - verifyResults(result, { foo: 2 }); - } - ); - }); - - it('supports collection groups', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - {}, - async collRef => { - 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 result = await query1.pipeline().execute(); - - verifyResults(result, { bar: 1 }, { foo: 1 }); - } + { title: 'Dune', rating: 4.6 } ); - }); - - it('supports query over collection path with special characters', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - {}, - async collRef => { - const docWithSpecials = doc(collRef, 'so!@#$%^&*()_+special'); - - const collectionWithSpecials = collection( - docWithSpecials, - 'so!@#$%^&*()_+special' - ); - await addDoc(collectionWithSpecials, { foo: 1 }); - await addDoc(collectionWithSpecials, { foo: 2 }); - - const result = await query( - collectionWithSpecials, - orderBy('foo', 'asc') - ) - .pipeline() - .execute(); - verifyResults(result, { foo: 1 }, { foo: 2 }); - } + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) ); - }); - - it('supports multiple inequality on same field', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, + expectResults( + snapshot, { - '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 } + title: 'Jonathan Strange & Mr Norrell', + rating: 4.6 }, - async collRef => { - const query1 = query( - collRef, - and(where('id', '>', 2), where('id', '<=', 10)) - ); - const result = await query1.pipeline().execute(); - verifyResults( - result, - { 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 } - ); - } + { title: 'The Master and Margarita', rating: 4.6 } ); - }); - it('supports multiple inequality on different fields', () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, { - '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 } + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 }, - async collRef => { - const query1 = query( - collRef, - and(where('id', '>=', 2), where('baz', '<', 2)) - ); - const result = await query1.pipeline().execute(); - verifyResults( - result, - { id: 4, foo: 1, bar: 2, baz: 1 }, - { id: 7, foo: 2, bar: 1, baz: 1 }, - { id: 10, foo: 2, bar: 2, baz: 1 } - ); + { + title: 'Pride and Prejudice', + rating: 4.5 } ); }); 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..ab29f8634e9 --- /dev/null +++ b/packages/firestore/test/integration/api/query_to_pipeline.test.ts @@ -0,0 +1,558 @@ +/** + * @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, + execute +} from '../util/firebase_export'; +import { + apiDescribe, + PERSISTENCE_MODE_UNSPECIFIED, + withTestCollection +} from '../util/helpers'; + +use(chaiAsPromised); + +setLogLevel('debug'); + +// 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 startAfter (with DocumentReference)', () => { + 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 DocumentReference)', () => { + 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 }); + } + ); + }); + + it('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 }); + } + ); + }); + + it('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 } + ); + } + ); + }); +}); 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..bb3edcda114 --- /dev/null +++ b/packages/firestore/test/integration/util/pipeline_export.ts @@ -0,0 +1,24 @@ +/** + * @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';