diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 4d33b925706..4a35ed07760 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -19,9 +19,9 @@ export { PipelineSource } from './lite-api/pipeline-source'; export { PipelineResult } from './lite-api/pipeline-result'; -export { Pipeline, pipeline } from './api/pipeline'; +export { Pipeline } from './api/pipeline'; -export { useFirestorePipelines } from './api/database_augmentation'; +export { useFirestorePipelines, pipeline } from './api/pipeline_impl'; export { execute } from './lite-api/pipeline_impl'; @@ -61,13 +61,13 @@ export { arrayContainsAny, arrayContainsAll, arrayLength, - inAny, - notInAny, + eqAny, + notEqAny, xor, - ifFunction, + cond, not, - logicalMax, - logicalMin, + logicalMaximum, + logicalMinimum, exists, isNan, reverse, @@ -92,8 +92,8 @@ export { avgFunction, andFunction, orFunction, - min, - max, + minimum, + maximum, cosineDistance, dotProduct, euclideanDistance, @@ -132,16 +132,17 @@ export { ArrayContainsAny, ArrayLength, ArrayElement, - In, + EqAny, + NotEqAny, IsNan, Exists, Not, And, Or, Xor, - If, - LogicalMax, - LogicalMin, + Cond, + LogicalMaximum, + LogicalMinimum, Reverse, ReplaceFirst, ReplaceAll, @@ -161,8 +162,8 @@ export { Count, Sum, Avg, - Min, - Max, + Minimum, + Maximum, CosineDistance, DotProduct, EuclideanDistance, diff --git a/packages/firestore/src/api/database_augmentation.ts b/packages/firestore/src/api/database_augmentation.ts index 0eb8c91a034..e69de29bb2d 100644 --- a/packages/firestore/src/api/database_augmentation.ts +++ b/packages/firestore/src/api/database_augmentation.ts @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Pipeline } from '../lite-api/pipeline'; -import { PipelineSource } from '../lite-api/pipeline-source'; -import { newUserDataReader } from '../lite-api/user_data_reader'; -import { DocumentKey } from '../model/document_key'; - -import { Firestore } from './database'; -import { DocumentReference, Query } from './reference'; -import { ExpUserDataWriter } from './user_data_writer'; - -export function useFirestorePipelines(): void { - Firestore.prototype.pipeline = function (): PipelineSource { - const firestore = this; - return new PipelineSource( - this, - newUserDataReader(firestore), - new ExpUserDataWriter(firestore), - (key: DocumentKey) => { - return new DocumentReference(firestore, null, key); - } - ); - }; - - Query.prototype.pipeline = function (): Pipeline { - let pipeline; - if (this._query.collectionGroup) { - pipeline = this.firestore - .pipeline() - .collectionGroup(this._query.collectionGroup); - } else { - pipeline = this.firestore - .pipeline() - .collection(this._query.path.canonicalString()); - } - - // TODO(pipeline) convert existing query filters, limits, etc into - // pipeline stages - - return pipeline; - }; -} diff --git a/packages/firestore/src/api/pipeline-source.ts b/packages/firestore/src/api/pipeline-source.ts new file mode 100644 index 00000000000..31a2d9d69a5 --- /dev/null +++ b/packages/firestore/src/api/pipeline-source.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PipelineSource as LitePipelineSoure } from '../lite-api/pipeline-source'; +import { + CollectionGroupSource, + CollectionSource, + DatabaseSource, + DocumentsSource +} from '../lite-api/stage'; +import { UserDataReader } from '../lite-api/user_data_reader'; +import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; +import { DocumentKey } from '../model/document_key'; +import { cast } from '../util/input_validation'; + +import { Firestore } from './database'; +import { Pipeline } from './pipeline'; +import { DocumentReference } from './reference'; + +/** + * Represents the source of a Firestore {@link Pipeline}. + * @beta + */ +export class PipelineSource extends LitePipelineSoure { + /** + * @internal + * @private + * @param db + * @param userDataReader + * @param userDataWriter + * @param documentReferenceFactory + */ + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + documentReferenceFactory: (id: DocumentKey) => DocumentReference + ) { + super(db, userDataReader, userDataWriter, documentReferenceFactory); + } + + collection(collectionPath: string): Pipeline { + const db = cast(this.db, Firestore); + return new Pipeline( + db, + this.userDataReader, + this.userDataWriter, + this.documentReferenceFactory, + [new CollectionSource(collectionPath)] + ); + } + + collectionGroup(collectionId: string): Pipeline { + const db = cast(this.db, Firestore); + return new Pipeline( + db, + this.userDataReader, + this.userDataWriter, + this.documentReferenceFactory, + [new CollectionGroupSource(collectionId)] + ); + } + + database(): Pipeline { + const db = cast(this.db, Firestore); + return new Pipeline( + db, + this.userDataReader, + this.userDataWriter, + this.documentReferenceFactory, + [new DatabaseSource()] + ); + } + + documents(docs: DocumentReference[]): Pipeline { + const db = cast(this.db, Firestore); + return new Pipeline( + db, + this.userDataReader, + this.userDataWriter, + this.documentReferenceFactory, + [DocumentsSource.of(docs)] + ); + } +} diff --git a/packages/firestore/src/api/pipeline.ts b/packages/firestore/src/api/pipeline.ts index 14532ba85c0..029e2d53eeb 100644 --- a/packages/firestore/src/api/pipeline.ts +++ b/packages/firestore/src/api/pipeline.ts @@ -15,18 +15,15 @@ * 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 { PipelineSource } from '../lite-api/pipeline-source'; -import { DocumentData, DocumentReference, Query } from '../lite-api/reference'; +import { DocumentData, 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 { DocumentKey } from '../model/document_key'; -import { cast } from '../util/input_validation'; -import { ensureFirestoreConfigured, Firestore } from './database'; +import { Firestore } from './database'; export class Pipeline< AppModelType = DocumentData @@ -61,6 +58,24 @@ export class Pipeline< ); } + protected newPipeline( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + documentReferenceFactory: (id: DocumentKey) => DocumentReference, + stages: Stage[], + converter: unknown = {} + ): Pipeline { + return new Pipeline( + db, + userDataReader, + userDataWriter, + documentReferenceFactory, + stages, + converter + ); + } + /** * Executes this pipeline and returns a Promise to represent the asynchronous operation. * @@ -93,43 +108,8 @@ export class Pipeline< * @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.map( - element => - new PipelineResult( - this.userDataWriter, - element.key?.path - ? this.documentReferenceFactory(element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - //this.converter - ) - ); - - return docs; - }); + throw new Error( + 'Pipelines not initialized. Your application must call `useFirestorePipelines()` before using Firestore Pipeline features.' + ); } } - -/** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - -/** - * Experimental Modular API for console testing. - * @param query - */ -export function pipeline(query: Query): Pipeline; - -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - return firestoreOrQuery.pipeline(); -} diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts new file mode 100644 index 00000000000..91163eff3c2 --- /dev/null +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pipeline } from '../api/pipeline'; +import { PipelineSource } from '../api/pipeline-source'; +import { PipelineResult } from '../api_pipelines'; +import { firestoreClientExecutePipeline } from '../core/firestore_client'; +import { newUserDataReader } from '../lite-api/user_data_reader'; +import { DocumentKey } from '../model/document_key'; +import { cast } from '../util/input_validation'; + +import { Firestore, ensureFirestoreConfigured } from './database'; +import { DocumentReference, Query } from './reference'; +import { ExpUserDataWriter } from './user_data_writer'; + +/** + * Experimental Modular API for console testing. + * @param firestore + */ +export function pipeline(firestore: Firestore): PipelineSource; + +/** + * Experimental Modular API for console testing. + * @param query + */ +export function pipeline(query: Query): Pipeline; + +export function pipeline( + firestoreOrQuery: Firestore | Query +): PipelineSource | Pipeline { + if (firestoreOrQuery instanceof Firestore) { + const firestore = firestoreOrQuery; + return new PipelineSource( + firestore, + newUserDataReader(firestore), + new ExpUserDataWriter(firestore), + (key: DocumentKey) => { + return new DocumentReference(firestore, null, key); + } + ); + } else { + let result; + const query = firestoreOrQuery; + const db = cast(query.firestore, Firestore); + if (query._query.collectionGroup) { + result = pipeline(db).collectionGroup(query._query.collectionGroup); + } else { + result = pipeline(db).collection(query._query.path.canonicalString()); + } + + // TODO(pipeline) convert existing query filters, limits, etc into + // pipeline stages + + return result; + } +} +export function useFirestorePipelines(): void { + Firestore.prototype.pipeline = function (): PipelineSource { + return pipeline(this); + }; + + Query.prototype.pipeline = function (): Pipeline { + return pipeline(this); + }; + + Pipeline.prototype.execute = function (): Promise { + return execute(this); + }; +} + +export function execute( + pipeline: Pipeline +): Promise>> { + const firestore = cast(pipeline._db, Firestore); + const client = ensureFirestoreConfigured(firestore); + return firestoreClientExecutePipeline(client, pipeline).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( + pipeline._userDataWriter, + element.key?.path + ? pipeline._documentReferenceFactory(element.key) + : undefined, + element.fields, + element.executionTime?.toTimestamp(), + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + //this.converter + ) + ); + + return docs; + }); +} diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts new file mode 100644 index 00000000000..658794098ea --- /dev/null +++ b/packages/firestore/src/api_pipelines.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { PipelineSource } from './lite-api/pipeline-source'; + +export { PipelineResult } from './lite-api/pipeline-result'; + +export { Pipeline } from './lite-api/pipeline'; + +export { useFirestorePipelines } from './api/database_augmentation'; + +export { + Stage, + FindNearestOptions, + AddFields, + Aggregate, + Distinct, + CollectionSource, + CollectionGroupSource, + DatabaseSource, + DocumentsSource, + Where, + FindNearest, + Limit, + Offset, + Select, + Sort, + GenericStage +} from './lite-api/stage'; + +export { + add, + subtract, + multiply, + divide, + mod, + eq, + neq, + lt, + lte, + gt, + gte, + arrayConcat, + arrayContains, + arrayContainsAny, + arrayContainsAll, + arrayLength, + eqAny, + notEqAny, + xor, + cond, + not, + logicalMaximum, + logicalMinimum, + exists, + isNan, + reverse, + replaceFirst, + replaceAll, + byteLength, + charLength, + like, + regexContains, + regexMatch, + strContains, + startsWith, + endsWith, + toLower, + toUpper, + trim, + strConcat, + mapGet, + countAll, + countFunction, + sumFunction, + avgFunction, + andFunction, + orFunction, + minimum, + maximum, + cosineDistance, + dotProduct, + euclideanDistance, + vectorLength, + unixMicrosToTimestamp, + timestampToUnixMicros, + unixMillisToTimestamp, + timestampToUnixMillis, + unixSecondsToTimestamp, + 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, + 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, + Ordering, + ExprType, + AccumulatorTarget, + FilterExpr, + SelectableExpr, + Selectable, + FilterCondition, + Accumulator +} from './lite-api/expressions'; diff --git a/packages/firestore/src/lite-api/database_augmentation.ts b/packages/firestore/src/lite-api/database_augmentation.ts index 14b9cf101c5..bf25e9c59c8 100644 --- a/packages/firestore/src/lite-api/database_augmentation.ts +++ b/packages/firestore/src/lite-api/database_augmentation.ts @@ -14,45 +14,3 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { DocumentKey } from '../model/document_key'; - -import { Firestore } from './database'; -import { Pipeline } from './pipeline'; -import { PipelineSource } from './pipeline-source'; -import { DocumentReference, Query } from './reference'; -import { LiteUserDataWriter } from './reference_impl'; -import { newUserDataReader } from './user_data_reader'; - -export function useFirestorePipelines(): void { - Firestore.prototype.pipeline = function (): PipelineSource { - const userDataWriter = new LiteUserDataWriter(this); - const userDataReader = newUserDataReader(this); - return new PipelineSource( - this, - userDataReader, - userDataWriter, - (key: DocumentKey) => { - return new DocumentReference(this, null, key); - } - ); - }; - - Query.prototype.pipeline = function (): Pipeline { - let pipeline; - if (this._query.collectionGroup) { - pipeline = this.firestore - .pipeline() - .collectionGroup(this._query.collectionGroup); - } else { - pipeline = this.firestore - .pipeline() - .collection(this._query.path.canonicalString()); - } - - // TODO(pipeline) convert existing query filters, limits, etc into - // pipeline stages - - return pipeline; - }; -} diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 26a2f7c617c..3ec68601e9e 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -804,13 +804,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").in("Electronics", Field.of("primaryType")); + * Field.of("category").eqAny("Electronics", Field.of("primaryType")); * ``` * * @param others The values or expressions to check against. * @return A new `Expr` representing the 'IN' comparison. */ - in(...others: Expr[]): In; + eqAny(...others: Expr[]): EqAny; /** * Creates an expression that checks if this expression is equal to any of the provided values or @@ -818,18 +818,52 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * Field.of("category").in("Electronics", Field.of("primaryType")); + * Field.of("category").eqAny("Electronics", Field.of("primaryType")); * ``` * * @param others The values or expressions to check against. * @return A new `Expr` representing the 'IN' comparison. */ - in(...others: any[]): In; - in(...others: any[]): In { + eqAny(...others: any[]): EqAny; + eqAny(...others: any[]): EqAny { const exprOthers = others.map(other => other instanceof Expr ? other : Constant.of(other) ); - return new In(this, exprOthers); + return new EqAny(this, exprOthers); + } + + /** + * Creates an expression that checks if this expression is not equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' + * Field.of("status").notEqAny("pending", Field.of("rejectedStatus")); + * ``` + * + * @param others The values or expressions to check against. + * @return A new `Expr` representing the 'NotEqAny' comparison. + */ + notEqAny(...others: Expr[]): NotEqAny; + + /** + * Creates an expression that checks if this expression is not equal to any of the provided values or + * expressions. + * + * ```typescript + * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' + * Field.of("status").notEqAny("pending", Field.of("rejectedStatus")); + * ``` + * + * @param others 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); } /** @@ -1300,13 +1334,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Find the lowest price of all products - * Field.of("price").min().as("lowestPrice"); + * Field.of("price").minimum().as("lowestPrice"); * ``` * * @return A new `Accumulator` representing the 'min' aggregation. */ - min(): Min { - return new Min(this, false); + minimum(): Minimum { + return new Minimum(this, false); } /** @@ -1314,13 +1348,13 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Find the highest score in a leaderboard - * Field.of("score").max().as("highestScore"); + * Field.of("score").maximum().as("highestScore"); * ``` * * @return A new `Accumulator` representing the 'max' aggregation. */ - max(): Max { - return new Max(this, false); + maximum(): Maximum { + return new Maximum(this, false); } /** @@ -1328,31 +1362,31 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Returns the larger value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMax(Function.currentTimestamp()); + * Field.of("timestamp").logicalMaximum(Function.currentTimestamp()); * ``` * * @param other The expression to compare with. * @return A new {@code Expr} representing the logical max operation. */ - logicalMax(other: Expr): LogicalMax; + 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").logicalMax(10); + * Field.of("value").logicalMaximum(10); * ``` * * @param other The constant value to compare with. * @return A new {@code Expr} representing the logical max operation. */ - logicalMax(other: any): LogicalMax; - logicalMax(other: any): LogicalMax { + logicalMaximum(other: any): LogicalMaximum; + logicalMaximum(other: any): LogicalMaximum { if (other instanceof Expr) { - return new LogicalMax(this, other as Expr); + return new LogicalMaximum(this, other as Expr); } - return new LogicalMax(this, Constant.of(other)); + return new LogicalMaximum(this, Constant.of(other)); } /** @@ -1360,31 +1394,31 @@ export abstract class Expr implements ProtoSerializable, UserData { * * ```typescript * // Returns the smaller value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMin(Function.currentTimestamp()); + * Field.of("timestamp").logicalMinimum(Function.currentTimestamp()); * ``` * * @param other The expression to compare with. * @return A new {@code Expr} representing the logical min operation. */ - logicalMin(other: Expr): LogicalMin; + 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").logicalMin(10); + * Field.of("value").logicalMinimum(10); * ``` * * @param other The constant value to compare with. * @return A new {@code Expr} representing the logical min operation. */ - logicalMin(other: any): LogicalMin; - logicalMin(other: any): LogicalMin { + logicalMinimum(other: any): LogicalMinimum; + logicalMinimum(other: any): LogicalMinimum { if (other instanceof Expr) { - return new LogicalMin(this, other as Expr); + return new LogicalMinimum(this, other as Expr); } - return new LogicalMin(this, Constant.of(other)); + return new LogicalMinimum(this, Constant.of(other)); } /** @@ -2472,9 +2506,19 @@ export class ArrayElement extends FirestoreFunction { /** * @beta */ -export class In extends FirestoreFunction implements FilterCondition { +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; +} + +/** + * @beta + */ +export class NotEqAny extends FirestoreFunction implements FilterCondition { constructor(private left: Expr, private others: Expr[]) { - super('in', [left, new ListOfExprs(others)]); + super('not_eq_any', [left, new ListOfExprs(others)]); } filterable = true as const; } @@ -2543,13 +2587,13 @@ export class Xor extends FirestoreFunction implements FilterCondition { /** * @beta */ -export class If extends FirestoreFunction implements FilterCondition { +export class Cond extends FirestoreFunction implements FilterCondition { constructor( private condition: FilterExpr, private thenExpr: Expr, private elseExpr: Expr ) { - super('if', [condition, thenExpr, elseExpr]); + super('cond', [condition, thenExpr, elseExpr]); } filterable = true as const; } @@ -2557,18 +2601,18 @@ export class If extends FirestoreFunction implements FilterCondition { /** * @beta */ -export class LogicalMax extends FirestoreFunction { +export class LogicalMaximum extends FirestoreFunction { constructor(private left: Expr, private right: Expr) { - super('logical_max', [left, right]); + super('logical_maximum', [left, right]); } } /** * @beta */ -export class LogicalMin extends FirestoreFunction { +export class LogicalMinimum extends FirestoreFunction { constructor(private left: Expr, private right: Expr) { - super('logical_min', [left, right]); + super('logical_minimum', [left, right]); } } @@ -2758,20 +2802,20 @@ export class Avg extends FirestoreFunction implements Accumulator { /** * @beta */ -export class Min extends FirestoreFunction implements Accumulator { +export class Minimum extends FirestoreFunction implements Accumulator { accumulator = true as const; constructor(private value: Expr, private distinct: boolean) { - super('min', [value]); + super('minimum', [value]); } } /** * @beta */ -export class Max extends FirestoreFunction implements Accumulator { +export class Maximum extends FirestoreFunction implements Accumulator { accumulator = true as const; constructor(private value: Expr, private distinct: boolean) { - super('max', [value]); + super('maximum', [value]); } } @@ -4378,14 +4422,14 @@ export function arrayLength(array: Expr): ArrayLength { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny(Field.of("category"), [Constant.of("Electronics"), Field.of("primaryType")]); + * eqAny(Field.of("category"), [Constant.of("Electronics"), Field.of("primaryType")]); * ``` * * @param element The expression to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function inAny(element: Expr, others: Expr[]): In; +export function eqAny(element: Expr, others: Expr[]): EqAny; /** * @beta @@ -4395,14 +4439,14 @@ export function inAny(element: Expr, others: Expr[]): In; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny(Field.of("category"), ["Electronics", Field.of("primaryType")]); + * eqAny(Field.of("category"), ["Electronics", Field.of("primaryType")]); * ``` * * @param element The expression to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function inAny(element: Expr, others: any[]): In; +export function eqAny(element: Expr, others: any[]): EqAny; /** * @beta @@ -4412,14 +4456,14 @@ export function inAny(element: Expr, others: any[]): In; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny("category", [Constant.of("Electronics"), Field.of("primaryType")]); + * eqAny("category", [Constant.of("Electronics"), Field.of("primaryType")]); * ``` * * @param element The field to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function inAny(element: string, others: Expr[]): In; +export function eqAny(element: string, others: Expr[]): EqAny; /** * @beta @@ -4429,20 +4473,20 @@ export function inAny(element: string, others: Expr[]): In; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny("category", ["Electronics", Field.of("primaryType")]); + * eqAny("category", ["Electronics", Field.of("primaryType")]); * ``` * * @param element The field to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function inAny(element: string, others: any[]): In; -export function inAny(element: Expr | string, others: any[]): In { +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 In(elementExpr, exprOthers); + return new EqAny(elementExpr, exprOthers); } /** @@ -4453,14 +4497,14 @@ export function inAny(element: Expr | string, others: any[]): In { * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny(Field.of("status"), [Constant.of("pending"), Field.of("rejectedStatus")]); + * notEqAny(Field.of("status"), [Constant.of("pending"), Field.of("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notInAny(element: Expr, others: Expr[]): Not; +export function notEqAny(element: Expr, others: Expr[]): NotEqAny; /** * @beta @@ -4470,14 +4514,14 @@ export function notInAny(element: Expr, others: Expr[]): Not; * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny(Field.of("status"), ["pending", Field.of("rejectedStatus")]); + * notEqAny(Field.of("status"), ["pending", Field.of("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notInAny(element: Expr, others: any[]): Not; +export function notEqAny(element: Expr, others: any[]): NotEqAny; /** * @beta @@ -4487,14 +4531,14 @@ export function notInAny(element: Expr, others: any[]): Not; * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny("status", [Constant.of("pending"), Field.of("rejectedStatus")]); + * notEqAny("status", [Constant.of("pending"), Field.of("rejectedStatus")]); * ``` * * @param element The field name to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notInAny(element: string, others: Expr[]): Not; +export function notEqAny(element: string, others: Expr[]): NotEqAny; /** * @beta @@ -4504,20 +4548,20 @@ export function notInAny(element: string, others: Expr[]): Not; * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny("status", ["pending", Field.of("rejectedStatus")]); + * notEqAny("status", ["pending", Field.of("rejectedStatus")]); * ``` * * @param element The field name to compare. * @param others The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notInAny(element: string, others: any[]): Not; -export function notInAny(element: Expr | string, others: any[]): Not { +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 Not(new In(elementExpr, exprOthers)); + return new NotEqAny(elementExpr, exprOthers); } /** @@ -4551,7 +4595,7 @@ export function xor(left: FilterExpr, ...right: FilterExpr[]): Xor { * * ```typescript * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". - * ifFunction( + * cond( * gt("age", 18), Constant.of("Adult"), Constant.of("Minor")); * ``` * @@ -4560,12 +4604,12 @@ export function xor(left: FilterExpr, ...right: FilterExpr[]): Xor { * @param elseExpr The expression to evaluate if the condition is false. * @return A new {@code Expr} representing the conditional expression. */ -export function ifFunction( +export function cond( condition: FilterExpr, thenExpr: Expr, elseExpr: Expr -): If { - return new If(condition, thenExpr, elseExpr); +): Cond { + return new Cond(condition, thenExpr, elseExpr); } /** @@ -4592,14 +4636,14 @@ export function not(filter: FilterExpr): Not { * * ```typescript * // Returns the larger value between the 'field1' field and the 'field2' field. - * logicalMax(Field.of("field1"), Field.of("field2")); + * 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 logicalMax(left: Expr, right: Expr): LogicalMax; +export function logicalMaximum(left: Expr, right: Expr): LogicalMaximum; /** * @beta @@ -4608,14 +4652,14 @@ export function logicalMax(left: Expr, right: Expr): LogicalMax; * * ```typescript * // Returns the larger value between the 'value' field and 10. - * logicalMax(Field.of("value"), 10); + * logicalMaximum(Field.of("value"), 10); * ``` * * @param left The left operand expression. * @param right The right operand constant. * @return A new {@code Expr} representing the logical max operation. */ -export function logicalMax(left: Expr, right: any): LogicalMax; +export function logicalMaximum(left: Expr, right: any): LogicalMaximum; /** * @beta @@ -4624,14 +4668,14 @@ export function logicalMax(left: Expr, right: any): LogicalMax; * * ```typescript * // Returns the larger value between the 'field1' field and the 'field2' field. - * logicalMax("field1", Field.of('field2')); + * logicalMaximum("field1", Field.of('field2')); * ``` * * @param left The left operand field name. * @param right The right operand expression. * @return A new {@code Expr} representing the logical max operation. */ -export function logicalMax(left: string, right: Expr): LogicalMax; +export function logicalMaximum(left: string, right: Expr): LogicalMaximum; /** * @beta @@ -4640,18 +4684,21 @@ export function logicalMax(left: string, right: Expr): LogicalMax; * * ```typescript * // Returns the larger value between the 'value' field and 10. - * logicalMax("value", 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 logicalMax(left: string, right: any): LogicalMax; -export function logicalMax(left: Expr | string, right: Expr | any): LogicalMax { +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 LogicalMax(normalizedLeft, normalizedRight); + return new LogicalMaximum(normalizedLeft, normalizedRight); } /** @@ -4661,14 +4708,14 @@ export function logicalMax(left: Expr | string, right: Expr | any): LogicalMax { * * ```typescript * // Returns the smaller value between the 'field1' field and the 'field2' field. - * logicalMin(Field.of("field1"), Field.of("field2")); + * 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 logicalMin(left: Expr, right: Expr): LogicalMin; +export function logicalMinimum(left: Expr, right: Expr): LogicalMinimum; /** * @beta @@ -4677,14 +4724,14 @@ export function logicalMin(left: Expr, right: Expr): LogicalMin; * * ```typescript * // Returns the smaller value between the 'value' field and 10. - * logicalMin(Field.of("value"), 10); + * logicalMinimum(Field.of("value"), 10); * ``` * * @param left The left operand expression. * @param right The right operand constant. * @return A new {@code Expr} representing the logical min operation. */ -export function logicalMin(left: Expr, right: any): LogicalMin; +export function logicalMinimum(left: Expr, right: any): LogicalMinimum; /** * @beta @@ -4693,14 +4740,14 @@ export function logicalMin(left: Expr, right: any): LogicalMin; * * ```typescript * // Returns the smaller value between the 'field1' field and the 'field2' field. - * logicalMin("field1", Field.of("field2")); + * logicalMinimum("field1", Field.of("field2")); * ``` * * @param left The left operand field name. * @param right The right operand expression. * @return A new {@code Expr} representing the logical min operation. */ -export function logicalMin(left: string, right: Expr): LogicalMin; +export function logicalMinimum(left: string, right: Expr): LogicalMinimum; /** * @beta @@ -4709,18 +4756,21 @@ export function logicalMin(left: string, right: Expr): LogicalMin; * * ```typescript * // Returns the smaller value between the 'value' field and 10. - * logicalMin("value", 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 logicalMin(left: string, right: any): LogicalMin; -export function logicalMin(left: Expr | string, right: Expr | any): LogicalMin { +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 LogicalMin(normalizedLeft, normalizedRight); + return new LogicalMinimum(normalizedLeft, normalizedRight); } /** @@ -5786,13 +5836,13 @@ export function avgFunction(value: Expr | string): Avg { * * ```typescript * // Find the lowest price of all products - * min(Field.of("price")).as("lowestPrice"); + * minimum(Field.of("price")).as("lowestPrice"); * ``` * * @param value The expression to find the minimum value of. * @return A new {@code Accumulator} representing the 'min' aggregation. */ -export function min(value: Expr): Min; +export function minimum(value: Expr): Minimum; /** * @beta @@ -5801,16 +5851,16 @@ export function min(value: Expr): Min; * * ```typescript * // Find the lowest price of all products - * min("price").as("lowestPrice"); + * 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. */ -export function min(value: string): Min; -export function min(value: Expr | string): Min { +export function minimum(value: string): Minimum; +export function minimum(value: Expr | string): Minimum { const exprValue = value instanceof Expr ? value : Field.of(value); - return new Min(exprValue, false); + return new Minimum(exprValue, false); } /** @@ -5821,13 +5871,13 @@ export function min(value: Expr | string): Min { * * ```typescript * // Find the highest score in a leaderboard - * max(Field.of("score")).as("highestScore"); + * maximum(Field.of("score")).as("highestScore"); * ``` * * @param value The expression to find the maximum value of. * @return A new {@code Accumulator} representing the 'max' aggregation. */ -export function max(value: Expr): Max; +export function maximum(value: Expr): Maximum; /** * @beta @@ -5836,16 +5886,16 @@ export function max(value: Expr): Max; * * ```typescript * // Find the highest score in a leaderboard - * max("score").as("highestScore"); + * 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. */ -export function max(value: string): Max; -export function max(value: Expr | string): Max { +export function maximum(value: string): Maximum; +export function maximum(value: Expr | string): Maximum { const exprValue = value instanceof Expr ? value : Field.of(value); - return new Max(exprValue, false); + return new Maximum(exprValue, false); } /** diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index a79c5cafc31..da62a229adc 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -43,10 +43,10 @@ export class PipelineSource { * @param documentReferenceFactory */ constructor( - private db: Firestore, - private userDataReader: UserDataReader, - private userDataWriter: AbstractUserDataWriter, - private documentReferenceFactory: (id: DocumentKey) => DocumentReference + protected db: Firestore, + protected userDataReader: UserDataReader, + protected userDataWriter: AbstractUserDataWriter, + protected documentReferenceFactory: (id: DocumentKey) => DocumentReference ) {} collection(collectionPath: string): Pipeline { diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 4d13d5dfddd..f835f709551 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -24,14 +24,12 @@ import { StructuredPipeline, Stage as ProtoStage } from '../protos/firestore_proto_api'; -import { invokeExecutePipeline } from '../remote/datastore'; import { getEncodedDatabaseId, JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; -import { getDatastore } from './components'; import { Firestore } from './database'; import { Accumulator, @@ -45,8 +43,7 @@ import { Selectable } from './expressions'; import { PipelineResult } from './pipeline-result'; -import { PipelineSource } from './pipeline-source'; -import { DocumentData, DocumentReference, Query } from './reference'; +import { DocumentData, DocumentReference } from './reference'; import { AddFields, Aggregate, @@ -130,8 +127,8 @@ export class Pipeline * @private * @param _db * @param userDataReader - * @param userDataWriter - * @param documentReferenceFactory + * @param _userDataWriter + * @param _documentReferenceFactory * @param stages * @param converter */ @@ -146,12 +143,12 @@ export class Pipeline * @internal * @private */ - protected userDataWriter: AbstractUserDataWriter, + public _userDataWriter: AbstractUserDataWriter, /** * @internal * @private */ - protected documentReferenceFactory: (id: DocumentKey) => DocumentReference, + public _documentReferenceFactory: (id: DocumentKey) => DocumentReference, private stages: Stage[], // TODO(pipeline) support converter //private converter: FirestorePipelineConverter = defaultPipelineConverter() @@ -191,11 +188,11 @@ export class Pipeline this.readUserData('addFields', this.selectablesToMap(fields)) ) ); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -237,11 +234,11 @@ export class Pipeline let projections: Map = this.selectablesToMap(selections); projections = this.readUserData('select', projections); copy.push(new Select(projections)); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -294,6 +291,24 @@ export class Pipeline return expressionMap; } + protected newPipeline( + db: Firestore, + userDataReader: UserDataReader, + userDataWriter: AbstractUserDataWriter, + documentReferenceFactory: (id: DocumentKey) => DocumentReference, + stages: Stage[], + converter: unknown = {} + ): Pipeline { + return new Pipeline( + db, + userDataReader, + userDataWriter, + documentReferenceFactory, + stages, + converter + ); + } + /** * Filters the documents from previous stages to only include those matching the specified {@link * FilterCondition}. @@ -329,11 +344,11 @@ export class Pipeline const copy = this.stages.map(s => s); this.readUserData('where', condition); copy.push(new Where(condition)); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -362,11 +377,11 @@ export class Pipeline offset(offset: number): Pipeline { const copy = this.stages.map(s => s); copy.push(new Offset(offset)); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -400,11 +415,11 @@ export class Pipeline limit(limit: number): Pipeline { const copy = this.stages.map(s => s); copy.push(new Limit(limit)); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -445,11 +460,11 @@ export class Pipeline this.readUserData('distinct', this.selectablesToMap(groups || [])) ) ); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -558,11 +573,11 @@ export class Pipeline ) ); } - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -586,11 +601,11 @@ export class Pipeline options.distanceField ) ); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy ); } @@ -647,11 +662,11 @@ export class Pipeline ); } - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -685,11 +700,11 @@ export class Pipeline } }); copy.push(new GenericStage(name, params)); - return new Pipeline( + return this.newPipeline( this._db, this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, + this._userDataWriter, + this._documentReferenceFactory, copy, this.converter ); @@ -754,7 +769,7 @@ export class Pipeline // converter: FirestorePipelineConverter | null // ): Pipeline { // const copy = this.stages.map(s => s); - // return new Pipeline( + // return this.newPipeline( // this.db, // copy, // converter ?? defaultPipelineConverter() @@ -793,29 +808,9 @@ export class Pipeline * @return A Promise representing the asynchronous pipeline execution. */ 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 - ? this.documentReferenceFactory(element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - //this.converter - ) - ); - - return docs; - }); + throw new Error( + 'Pipelines not initialized. Your application must call `useFirestorePipelines()` before using Firestore Pipeline features.' + ); } /** @@ -833,21 +828,3 @@ export class Pipeline }; } } - -/** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - -/** - * Experimental Modular API for console testing. - * @param query - */ -export function pipeline(query: Query): Pipeline; - -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - return firestoreOrQuery.pipeline(); -} diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index cd490d31871..d049bbba05c 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -15,8 +15,17 @@ * limitations under the License. */ +import { DocumentKey } from '../model/document_key'; +import { invokeExecutePipeline } from '../remote/datastore'; + +import { getDatastore } from './components'; +import { Firestore } from './database'; import { Pipeline } from './pipeline'; import { PipelineResult } from './pipeline-result'; +import { PipelineSource } from './pipeline-source'; +import { DocumentReference, Query } from './reference'; +import { LiteUserDataWriter } from './reference_impl'; +import { newUserDataReader } from './user_data_reader'; /** * Modular API for console experimentation. @@ -26,5 +35,84 @@ import { PipelineResult } from './pipeline-result'; export function execute( pipeline: Pipeline ): Promise>> { - return pipeline.execute(); + const datastore = getDatastore(pipeline._db); + return invokeExecutePipeline(datastore, pipeline).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( + pipeline._userDataWriter, + element.key?.path + ? pipeline._documentReferenceFactory(element.key) + : undefined, + element.fields, + element.executionTime?.toTimestamp(), + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + //this.converter + ) + ); + + return docs; + }); +} + +/** + * Experimental Modular API for console testing. + * @param firestore + */ +export function pipeline(firestore: Firestore): PipelineSource; + +/** + * Experimental Modular API for console testing. + * @param query + */ +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( + db, + userDataReader, + userDataWriter, + (key: DocumentKey) => { + return new DocumentReference(db, null, key); + } + ); + } 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()); + } + + // TODO(pipeline) convert existing query filters, limits, etc into + // pipeline stages + + return pipeline; + } +} + +export function useFirestorePipelines(): void { + Firestore.prototype.pipeline = function (): PipelineSource { + return pipeline(this); + }; + + Query.prototype.pipeline = function (): Pipeline { + return pipeline(this); + }; } diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index a954d9b53e1..22fb5a1cdfb 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -55,14 +55,21 @@ import { setDoc, startsWith, subtract, - useFirestorePipelines + useFirestorePipelines, + setLogLevel, + cond, + eqAny, + logicalMaximum, + logicalMinimum, + notEqAny } from '../util/firebase_export'; import { apiDescribe, withTestCollection } from '../util/helpers'; use(chaiAsPromised); -useFirestorePipelines(); -apiDescribe.skip('Pipelines', persistence => { +setLogLevel('debug'); + +apiDescribe.only('Pipelines', persistence => { addEqualityMatcher(); let firestore: Firestore; let randomCol: CollectionReference; @@ -258,595 +265,689 @@ apiDescribe.skip('Pipelines', persistence => { await withTestCollectionPromise; }); - // setLogLevel('debug') + describe('fluent API', () => { + before(() => { + useFirestorePipelines(); + }); - it('empty results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .limit(0) - .execute(); - expect(result.length).to.equal(0); - }); + it('empty results as expected', async () => { + const result = await firestore + .pipeline() + .collection(randomCol.path) + .limit(0) + .execute(); + expect(result.length).to.equal(0); + }); - it('full results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .execute(); - expect(result.length).to.equal(10); - }); + it('full results as expected', async () => { + const result = await firestore + .pipeline() + .collection(randomCol.path) + .execute(); + expect(result.length).to.equal(10); + }); - it('returns aggregate results as expected', async () => { - let result = await firestore - .pipeline() - .collection(randomCol.path) - .aggregate(countAll().as('count')) - .execute(); - expectResults(result, { count: 10 }); - - result = await randomCol - .pipeline() - .where(eq('genre', 'Science Fiction')) - .aggregate( - countAll().as('count'), - avgFunction('rating').as('avgRating'), - Field.of('rating').max().as('maxRating') - ) - .execute(); - expectResults(result, { count: 2, avgRating: 4.4, maxRating: 4.6 }); - }); + it('returns aggregate results as expected', async () => { + let result = await firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countAll().as('count')) + .execute(); + expectResults(result, { count: 10 }); + + 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 }); + }); - it('rejects groups without accumulators', async () => { - await expect( - randomCol + it('rejects groups without accumulators', async () => { + await expect( + randomCol + .pipeline() + .where(lt('published', 1900)) + .aggregate({ + accumulators: [], + groups: ['genre'] + }) + .execute() + ).to.be.rejected; + }); + + // skip: toLower not supported + // it.skip('returns distinct values as expected', async () => { + // const results = await randomCol + // .pipeline() + // .where(lt('published', 1900)) + // .distinct(Field.of('genre').toLower().as('lowerGenre')) + // .execute(); + // expectResults( + // results, + // { lowerGenre: 'romance' }, + // { lowerGenre: 'psychological thriller' } + // ); + // }); + + it('returns group and accumulate results', async () => { + const results = await randomCol .pipeline() - .where(lt('published', 1900)) + .where(lt(Field.of('published'), 1984)) .aggregate({ - accumulators: [], + accumulators: [avgFunction('rating').as('avgRating')], groups: ['genre'] }) - .execute() - ).to.be.rejected; - }); - - // skip: toLower not supported - // it.skip('returns distinct values as expected', async () => { - // const results = await randomCol - // .pipeline() - // .where(lt('published', 1900)) - // .distinct(Field.of('genre').toLower().as('lowerGenre')) - // .execute(); - // expectResults( - // results, - // { lowerGenre: 'romance' }, - // { lowerGenre: 'psychological thriller' } - // ); - // }); - - 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' } - ); - }); + .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').max().as('maxRating'), - Field.of('published').min().as('minPublished') - ) - .execute(); - expectResults(results, { - count: 10, - maxRating: 4.7, - minPublished: 1813 + 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 + }); }); - }); - it('can select fields', async () => { - const results = await firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'author') - .sort(Field.of('author').ascending()) - .execute(); - expectResults( - results, - { - title: "The Hitchhiker's Guide to the Galaxy", - author: 'Douglas Adams' - }, - { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, - { title: 'Dune', author: 'Frank Herbert' }, - { title: 'Crime and Punishment', author: 'Fyodor Dostoevsky' }, - { - title: 'One Hundred Years of Solitude', - author: 'Gabriel García Márquez' - }, - { title: '1984', author: 'George Orwell' }, - { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, - { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' }, - { title: 'Pride and Prejudice', author: 'Jane Austen' }, - { title: "The Handmaid's Tale", author: 'Margaret Atwood' } - ); - }); + it('can select fields', async () => { + const results = await firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(Field.of('author').ascending()) + .execute(); + expectResults( + results, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams' + }, + { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, + { title: 'Dune', author: 'Frank Herbert' }, + { title: 'Crime and Punishment', author: 'Fyodor Dostoevsky' }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez' + }, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' }, + { title: 'Pride and Prejudice', author: 'Jane Austen' }, + { title: "The Handmaid's Tale", author: 'Margaret Atwood' } + ); + }); - it('where with and', async () => { - const results = await randomCol - .pipeline() - .where(andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction'))) - .execute(); - expectResults(results, 'book10'); - }); + it('where with and', async () => { + const results = await randomCol + .pipeline() + .where(andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction'))) + .execute(); + expectResults(results, 'book10'); + }); - it('where with or', async () => { - const results = await randomCol - .pipeline() - .where(orFunction(eq('genre', 'Romance'), eq('genre', 'Dystopian'))) - .select('title') - .execute(); - expectResults( - results, - { title: 'Pride and Prejudice' }, - { title: "The Handmaid's Tale" }, - { title: '1984' } - ); - }); + it('where with or', async () => { + const results = await randomCol + .pipeline() + .where(orFunction(eq('genre', 'Romance'), eq('genre', 'Dystopian'))) + .select('title') + .execute(); + expectResults( + results, + { title: 'Pride and Prejudice' }, + { title: "The Handmaid's Tale" }, + { title: '1984' } + ); + }); - 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' } - ); - }); + 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' } + ); + }); - it('arrayContains works', async () => { - const results = await randomCol - .pipeline() - .where(arrayContains('tags', 'comedy')) - .select('title') - .execute(); - expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); - }); + it('logical min works', async () => { + const results = await randomCol + .pipeline() + .select( + 'title', + logicalMinimum(Constant.of(1960), Field.of('published')).as( + 'published-safe' + ) + ) + .sort(Field.of('title').ascending()) + .limit(3) + .execute(); + expectResults( + results, + { title: '1984', 'published-safe': 1949 }, + { title: 'Crime and Punishment', 'published-safe': 1866 }, + { title: 'Dune', 'published-safe': 1960 } + ); + }); - it('arrayContainsAny works', async () => { - const results = await randomCol - .pipeline() - .where(arrayContainsAny('tags', ['comedy', 'classic'])) - .select('title') - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'Pride and Prejudice' } - ); - }); + it('logical max works', async () => { + const results = await randomCol + .pipeline() + .select( + 'title', + logicalMaximum(Constant.of(1960), Field.of('published')).as( + 'published-safe' + ) + ) + .sort(Field.of('title').ascending()) + .limit(3) + .execute(); + expectResults( + results, + { title: '1984', 'published-safe': 1960 }, + { title: 'Crime and Punishment', 'published-safe': 1960 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); - it('arrayContainsAll works', async () => { - const results = await randomCol - .pipeline() - .where(Field.of('tags').arrayContainsAll('adventure', 'magic')) - .select('title') - .execute(); - expectResults(results, { title: 'The Lord of the Rings' }); - }); + it('cond works', async () => { + const results = await randomCol + .pipeline() + .select( + 'title', + cond( + lt(Field.of('published'), 1960), + Constant.of(1960), + Field.of('published') + ).as('published-safe') + ) + .sort(Field.of('title').ascending()) + .limit(3) + .execute(); + expectResults( + results, + { title: '1984', 'published-safe': 1960 }, + { title: 'Crime and Punishment', 'published-safe': 1960 }, + { title: 'Dune', 'published-safe': 1965 } + ); + }); - it('arrayLength works', async () => { - const results = await randomCol - .pipeline() - .select(Field.of('tags').arrayLength().as('tagsCount')) - .where(eq('tagsCount', 3)) - .execute(); - expect(results.length).to.equal(10); - }); + it('eqAny works', async () => { + const results = await randomCol + .pipeline() + .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' } + ); + }); - // 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 - .pipeline() - .select( - Field.of('author').strConcat(' - ', Field.of('title')).as('bookInfo') - ) - .limit(1) - .execute(); - expectResults(results, { - bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" + it('notEqAny works', async () => { + const results = await randomCol + .pipeline() + .where( + notEqAny( + 'published', + [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] + ) + ) + .select('title') + .execute(); + expectResults(results, { title: 'Pride and Prejudice' }); }); - }); - it('testStartsWith', async () => { - const results = await randomCol - .pipeline() - .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' } - ); - }); + it('arrayContains works', async () => { + const results = await randomCol + .pipeline() + .where(arrayContains('tags', 'comedy')) + .select('title') + .execute(); + expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); + }); - it('testEndsWith', async () => { - const results = await randomCol - .pipeline() - .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' } - ); - }); + it('arrayContainsAny works', async () => { + const results = await randomCol + .pipeline() + .where(arrayContainsAny('tags', ['comedy', 'classic'])) + .select('title') + .execute(); + expectResults( + results, + { title: "The Hitchhiker's Guide to the Galaxy" }, + { title: 'Pride and Prejudice' } + ); + }); - it('testLength', async () => { - const results = await randomCol - .pipeline() - .select( - Field.of('title').charLength().as('titleLength'), - Field.of('title') - ) - .where(gt('titleLength', 20)) - .sort(Field.of('title').ascending()) - .execute(); - - expectResults( - results, - - { - titleLength: 29, - title: 'One Hundred Years of Solitude' - }, - { - titleLength: 36, - title: "The Hitchhiker's Guide to the Galaxy" - }, - { - titleLength: 21, - title: 'The Lord of the Rings' - }, - { - titleLength: 21, - title: 'To Kill a Mockingbird' - } - ); - }); + it('arrayContainsAll works', async () => { + const results = await randomCol + .pipeline() + .where(Field.of('tags').arrayContainsAll('adventure', 'magic')) + .select('title') + .execute(); + expectResults(results, { title: 'The Lord of the Rings' }); + }); - // 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 - .pipeline() - .where(like('title', '%Guide%')) - .select('title') - .execute(); - expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); - }); + it('arrayLength works', async () => { + const results = await randomCol + .pipeline() + .select(Field.of('tags').arrayLength().as('tagsCount')) + .where(eq('tagsCount', 3)) + .execute(); + expect(results.length).to.equal(10); + }); - it('testRegexContains', async () => { - const results = await randomCol - .pipeline() - .where(regexContains('title', '(?i)(the|of)')) - .execute(); - expect(results.length).to.equal(5); - }); + // 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 + .pipeline() + .select( + Field.of('author').strConcat(' - ', Field.of('title')).as('bookInfo') + ) + .limit(1) + .execute(); + expectResults(results, { + bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" + }); + }); - it('testRegexMatches', async () => { - const results = await randomCol - .pipeline() - .where(regexMatch('title', '.*(?i)(the|of).*')) - .execute(); - expect(results.length).to.equal(5); - }); + it('testStartsWith', async () => { + const results = await randomCol + .pipeline() + .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' } + ); + }); - it('testArithmeticOperations', async () => { - const results = await randomCol - .pipeline() - .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') - ) - .limit(1) - .execute(); - expectResults(results, { - ratingPlusOne: 5.2, - yearsSince1900: 79, - ratingTimesTen: 42, - ratingDividedByTwo: 2.1 + it('testEndsWith', async () => { + const results = await randomCol + .pipeline() + .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' } + ); }); - }); - it('testComparisonOperators', async () => { - const results = await randomCol - .pipeline() - .where( - andFunction( - gt('rating', 4.2), - lte(Field.of('rating'), 4.5), - neq('genre', 'Science Fiction') + it('testLength', async () => { + const results = await randomCol + .pipeline() + .select( + Field.of('title').charLength().as('titleLength'), + Field.of('title') ) - ) - .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' } - ); - }); + .where(gt('titleLength', 20)) + .sort(Field.of('title').ascending()) + .execute(); + + expectResults( + results, + + { + titleLength: 29, + title: 'One Hundred Years of Solitude' + }, + { + titleLength: 36, + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + titleLength: 21, + title: 'The Lord of the Rings' + }, + { + titleLength: 21, + title: 'To Kill a Mockingbird' + } + ); + }); - it('testLogicalOperators', async () => { - const results = await randomCol - .pipeline() - .where( - orFunction( - andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) + // 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 + .pipeline() + .where(like('title', '%Guide%')) + .select('title') + .execute(); + expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); + }); + + it('testRegexContains', async () => { + const results = await randomCol + .pipeline() + .where(regexContains('title', '(?i)(the|of)')) + .execute(); + expect(results.length).to.equal(5); + }); + + it('testRegexMatches', async () => { + const results = await randomCol + .pipeline() + .where(regexMatch('title', '.*(?i)(the|of).*')) + .execute(); + expect(results.length).to.equal(5); + }); + + it('testArithmeticOperations', async () => { + const results = await randomCol + .pipeline() + .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') ) - ) - .select('title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { title: 'Crime and Punishment' }, - { title: 'Dune' }, - { title: 'Pride and Prejudice' } - ); - }); + .limit(1) + .execute(); + expectResults(results, { + ratingPlusOne: 5.2, + yearsSince1900: 79, + ratingTimesTen: 42, + ratingDividedByTwo: 2.1 + }); + }); - it('testChecks', async () => { - const results = await randomCol - .pipeline() - .where(not(Field.of('rating').isNaN())) - .select( - Field.of('rating').eq(null).as('ratingIsNull'), - not(Field.of('rating').isNaN()).as('ratingIsNotNaN') - ) - .limit(1) - .execute(); - expectResults(results, { ratingIsNull: false, ratingIsNotNaN: true }); - }); + it('testComparisonOperators', async () => { + const results = await randomCol + .pipeline() + .where( + andFunction( + gt('rating', 4.2), + lte(Field.of('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' } + ); + }); - it('testMapGet', async () => { - const results = await randomCol - .pipeline() - .select( - Field.of('awards').mapGet('hugo').as('hugoAward'), - Field.of('awards').mapGet('others').as('others'), - Field.of('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 } - ); - }); + it('testLogicalOperators', async () => { + const results = await randomCol + .pipeline() + .where( + orFunction( + andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction')), + lt('published', 1900) + ) + ) + .select('title') + .sort(Field.of('title').ascending()) + .execute(); + expectResults( + results, + { title: 'Crime and Punishment' }, + { title: 'Dune' }, + { title: 'Pride and Prejudice' } + ); + }); - // 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 - .pipeline() - .select( - cosineDistance(Constant.vector(sourceVector), targetVector).as( - 'cosineDistance' - ), - dotProduct(Constant.vector(sourceVector), targetVector).as( - 'dotProductDistance' - ), - euclideanDistance(Constant.vector(sourceVector), targetVector).as( - 'euclideanDistance' + it('testChecks', async () => { + const results = await randomCol + .pipeline() + .where(not(Field.of('rating').isNaN())) + .select( + Field.of('rating').eq(null).as('ratingIsNull'), + not(Field.of('rating').isNaN()).as('ratingIsNotNaN') ) - ) - .limit(1) - .execute(); + .limit(1) + .execute(); + expectResults(results, { ratingIsNull: false, ratingIsNotNaN: true }); + }); - expectResults(results, { - cosineDistance: 0.02560880430538015, - dotProductDistance: 0.13, - euclideanDistance: 0.806225774829855 + it('testMapGet', async () => { + const results = await randomCol + .pipeline() + .select( + Field.of('awards').mapGet('hugo').as('hugoAward'), + Field.of('awards').mapGet('others').as('others'), + Field.of('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 } + ); }); - }); - it('testNestedFields', async () => { - const results = await randomCol - .pipeline() - .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 } - ); - }); + // 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 + .pipeline() + .select( + cosineDistance(Constant.vector(sourceVector), targetVector).as( + 'cosineDistance' + ), + dotProduct(Constant.vector(sourceVector), targetVector).as( + 'dotProductDistance' + ), + euclideanDistance(Constant.vector(sourceVector), targetVector).as( + 'euclideanDistance' + ) + ) + .limit(1) + .execute(); + + expectResults(results, { + cosineDistance: 0.02560880430538015, + dotProductDistance: 0.13, + euclideanDistance: 0.806225774829855 + }); + }); - it('test mapGet with field name including . notation', async () => { - const results = await randomCol - .pipeline() - .where(eq('awards.hugo', true)) - .select( - 'title', - Field.of('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 } - ); - }); + it('testNestedFields', async () => { + const results = await randomCol + .pipeline() + .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 } + ); + }); - 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('test mapGet with field name including . notation', async () => { + const results = await randomCol + .pipeline() + .where(eq('awards.hugo', true)) + .select( + 'title', + Field.of('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 } ); + }); + + 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') + ); + + const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); + expect(proto).not.to.be.null; + }); - const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; + // TODO(pipeline) support converter + // it('pipeline converter works', async () => { + // interface AppModel {myTitle: string; myAuthor: string; myPublished: number} + // const converter: FirestorePipelineConverter = { + // fromFirestore(result: FirebaseFirestore.PipelineResult): AppModel { + // return { + // myTitle: result.data()!.title as string, + // myAuthor: result.data()!.author as string, + // myPublished: result.data()!.published as number, + // }; + // }, + // }; + // + // const results = await firestore + // .pipeline() + // .collection(randomCol.path) + // .sort(Field.of('published').ascending()) + // .limit(2) + // .withConverter(converter) + // .execute(); + // + // const objs = results.map(r => r.data()); + // expect(objs[0]).to.deep.equal({ + // myAuthor: 'Jane Austen', + // myPublished: 1813, + // myTitle: 'Pride and Prejudice', + // }); + // expect(objs[1]).to.deep.equal({ + // myAuthor: 'Fyodor Dostoevsky', + // myPublished: 1866, + // myTitle: 'Crime and Punishment', + // }); + // }); }); - // TODO(pipeline) support converter - // it('pipeline converter works', async () => { - // interface AppModel {myTitle: string; myAuthor: string; myPublished: number} - // const converter: FirestorePipelineConverter = { - // fromFirestore(result: FirebaseFirestore.PipelineResult): AppModel { - // return { - // myTitle: result.data()!.title as string, - // myAuthor: result.data()!.author as string, - // myPublished: result.data()!.published as number, - // }; - // }, - // }; - // - // const results = await firestore - // .pipeline() - // .collection(randomCol.path) - // .sort(Field.of('published').ascending()) - // .limit(2) - // .withConverter(converter) - // .execute(); - // - // const objs = results.map(r => r.data()); - // expect(objs[0]).to.deep.equal({ - // myAuthor: 'Jane Austen', - // myPublished: 1813, - // myTitle: 'Pride and Prejudice', - // }); - // expect(objs[1]).to.deep.equal({ - // myAuthor: 'Fyodor Dostoevsky', - // myPublished: 1866, - // myTitle: 'Crime and Punishment', - // }); - // }); describe('modular API', () => { it('works when creating a pipeline from a Firestore instance', async () => { const myPipeline = pipeline(firestore) diff --git a/repo-scripts/size-analysis/bundle-definitions/firestore.json b/repo-scripts/size-analysis/bundle-definitions/firestore.json index 6c1adcad52c..f265521b08b 100644 --- a/repo-scripts/size-analysis/bundle-definitions/firestore.json +++ b/repo-scripts/size-analysis/bundle-definitions/firestore.json @@ -129,7 +129,39 @@ ] }, { - "name": "Pipeline Query with lt filter", + "name": "Pipeline Query with lt filter (execute)", + "dependencies": [ + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "app", + "imports": [ + "initializeApp" + ] + } + ] + }, + { + "packageName": "firebase", + "versionOrTag": "latest", + "imports": [ + { + "path": "firestore", + "imports": [ + "getFirestore", + "lt", + "Field", + "execute" + ] + } + ] + } + ] + }, + { + "name": "Pipeline Query with lt filter (useFirestorePipelines)", "dependencies": [ { "packageName": "firebase",